lens-engine 0.1.18 → 0.1.20
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/cli.js +672 -520
- package/daemon.js +1576 -1485
- package/dashboard/assets/index-D5Z-3_m6.js +341 -0
- package/dashboard/assets/index-DmLHTAhP.css +1 -0
- package/dashboard/index.html +2 -2
- package/package.json +4 -1
- package/dashboard/assets/index-B-FwfnUH.css +0 -1
- package/dashboard/assets/index-Dpgp0uUn.js +0 -341
package/cli.js
CHANGED
|
@@ -3347,36 +3347,108 @@ var {
|
|
|
3347
3347
|
Help
|
|
3348
3348
|
} = import_index.default;
|
|
3349
3349
|
|
|
3350
|
-
// packages/cli/src/util/
|
|
3351
|
-
import {
|
|
3352
|
-
import
|
|
3353
|
-
import
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3350
|
+
// packages/cli/src/util/config.ts
|
|
3351
|
+
import { randomUUID } from "crypto";
|
|
3352
|
+
import fsSync from "fs";
|
|
3353
|
+
import fs from "fs/promises";
|
|
3354
|
+
import os from "os";
|
|
3355
|
+
import path from "path";
|
|
3356
|
+
var CONFIG_DIR = path.join(os.homedir(), ".lens");
|
|
3357
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
3358
|
+
var DEFAULTS = {
|
|
3359
|
+
inject_behavior: "always",
|
|
3360
|
+
show_progress: true
|
|
3361
|
+
};
|
|
3362
|
+
async function readConfig() {
|
|
3363
|
+
try {
|
|
3364
|
+
const raw = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
3365
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
3366
|
+
} catch {
|
|
3367
|
+
return DEFAULTS;
|
|
3361
3368
|
}
|
|
3362
3369
|
}
|
|
3363
|
-
async function
|
|
3370
|
+
async function writeConfig(config) {
|
|
3371
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
3372
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
3373
|
+
}
|
|
3374
|
+
async function configGet(key) {
|
|
3375
|
+
const config = await readConfig();
|
|
3376
|
+
if (key in config) {
|
|
3377
|
+
return JSON.stringify(config[key], null, 2);
|
|
3378
|
+
}
|
|
3379
|
+
return "null";
|
|
3380
|
+
}
|
|
3381
|
+
async function configSet(key, value) {
|
|
3382
|
+
const config = await readConfig();
|
|
3383
|
+
if (key === "inject_behavior" && ["always", "skip", "once"].includes(value)) {
|
|
3384
|
+
config.inject_behavior = value;
|
|
3385
|
+
} else if (key === "show_progress") {
|
|
3386
|
+
config.show_progress = value === "true";
|
|
3387
|
+
} else if (key === "cloud_url") {
|
|
3388
|
+
config.cloud_url = value;
|
|
3389
|
+
} else if (key === "telemetry") {
|
|
3390
|
+
config.telemetry = value === "true";
|
|
3391
|
+
} else {
|
|
3392
|
+
throw new Error(`Invalid config: ${key}=${value}`);
|
|
3393
|
+
}
|
|
3394
|
+
await writeConfig(config);
|
|
3395
|
+
}
|
|
3396
|
+
function getCloudUrl() {
|
|
3397
|
+
if (process.env.LENS_CLOUD_URL) return process.env.LENS_CLOUD_URL;
|
|
3364
3398
|
try {
|
|
3365
|
-
const
|
|
3366
|
-
|
|
3367
|
-
return match?.[1]?.trim();
|
|
3399
|
+
const cfg2 = JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8"));
|
|
3400
|
+
if (cfg2.cloud_url) return cfg2.cloud_url;
|
|
3368
3401
|
} catch {
|
|
3369
|
-
|
|
3402
|
+
}
|
|
3403
|
+
return "https://cloud.lens-engine.com";
|
|
3404
|
+
}
|
|
3405
|
+
function readConfigSync() {
|
|
3406
|
+
try {
|
|
3407
|
+
return { ...DEFAULTS, ...JSON.parse(fsSync.readFileSync(CONFIG_FILE, "utf-8")) };
|
|
3408
|
+
} catch {
|
|
3409
|
+
return DEFAULTS;
|
|
3370
3410
|
}
|
|
3371
3411
|
}
|
|
3372
|
-
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
3412
|
+
function isTelemetryEnabled() {
|
|
3413
|
+
const config = readConfigSync();
|
|
3414
|
+
return config.telemetry !== false;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// packages/cli/src/util/format.ts
|
|
3418
|
+
function output(data, json) {
|
|
3419
|
+
if (json) {
|
|
3420
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3421
|
+
`);
|
|
3422
|
+
} else if (typeof data === "string") {
|
|
3423
|
+
process.stdout.write(`${data}
|
|
3424
|
+
`);
|
|
3425
|
+
} else {
|
|
3426
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
3427
|
+
`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
function error(msg) {
|
|
3431
|
+
process.stderr.write(`Error: ${msg}
|
|
3432
|
+
`);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// packages/cli/src/commands/config.ts
|
|
3436
|
+
async function configGetCommand(key) {
|
|
3437
|
+
try {
|
|
3438
|
+
const value = await configGet(key);
|
|
3439
|
+
output(value, false);
|
|
3440
|
+
} catch (err) {
|
|
3441
|
+
output(err instanceof Error ? err.message : String(err), false);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
async function configSetCommand(key, value) {
|
|
3445
|
+
try {
|
|
3446
|
+
await configSet(key, value);
|
|
3447
|
+
output(`Config updated: ${key} = ${value}`, false);
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
output(err instanceof Error ? err.message : String(err), false);
|
|
3450
|
+
process.exit(1);
|
|
3376
3451
|
}
|
|
3377
|
-
const remote_url = await parseRemoteUrl(root);
|
|
3378
|
-
const name = basename(root);
|
|
3379
|
-
return { root_path: root, name, remote_url };
|
|
3380
3452
|
}
|
|
3381
3453
|
|
|
3382
3454
|
// packages/cli/src/util/client.ts
|
|
@@ -3434,159 +3506,336 @@ async function request(method, path4, body, retries = 3) {
|
|
|
3434
3506
|
var get = (path4) => request("GET", path4);
|
|
3435
3507
|
var post = (path4, body) => request("POST", path4, body);
|
|
3436
3508
|
|
|
3437
|
-
// packages/cli/src/util/
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3509
|
+
// packages/cli/src/util/repo.ts
|
|
3510
|
+
import { existsSync } from "fs";
|
|
3511
|
+
import { readFile } from "fs/promises";
|
|
3512
|
+
import { basename, dirname, resolve } from "path";
|
|
3513
|
+
function findGitRoot(from) {
|
|
3514
|
+
let dir = resolve(from);
|
|
3515
|
+
while (true) {
|
|
3516
|
+
if (existsSync(resolve(dir, ".git"))) return dir;
|
|
3517
|
+
const parent = dirname(dir);
|
|
3518
|
+
if (parent === dir) return null;
|
|
3519
|
+
dir = parent;
|
|
3445
3520
|
}
|
|
3446
3521
|
}
|
|
3447
|
-
function
|
|
3448
|
-
process.stderr.write(`Error: ${msg}
|
|
3449
|
-
`);
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
// packages/cli/src/util/inject-claude-md.ts
|
|
3453
|
-
import fs2 from "fs/promises";
|
|
3454
|
-
import path2 from "path";
|
|
3455
|
-
|
|
3456
|
-
// packages/cli/src/util/config.ts
|
|
3457
|
-
import fs from "fs/promises";
|
|
3458
|
-
import fsSync from "fs";
|
|
3459
|
-
import path from "path";
|
|
3460
|
-
import os from "os";
|
|
3461
|
-
import { randomUUID } from "crypto";
|
|
3462
|
-
var CONFIG_DIR = path.join(os.homedir(), ".lens");
|
|
3463
|
-
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
3464
|
-
var DEFAULTS = {
|
|
3465
|
-
inject_behavior: "always",
|
|
3466
|
-
show_progress: true
|
|
3467
|
-
};
|
|
3468
|
-
async function readConfig() {
|
|
3522
|
+
async function parseRemoteUrl(gitRoot) {
|
|
3469
3523
|
try {
|
|
3470
|
-
const
|
|
3471
|
-
|
|
3524
|
+
const config = await readFile(resolve(gitRoot, ".git/config"), "utf-8");
|
|
3525
|
+
const match = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/m);
|
|
3526
|
+
return match?.[1]?.trim();
|
|
3472
3527
|
} catch {
|
|
3473
|
-
return
|
|
3528
|
+
return void 0;
|
|
3474
3529
|
}
|
|
3475
3530
|
}
|
|
3476
|
-
async function
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
async function configGet(key) {
|
|
3481
|
-
const config = await readConfig();
|
|
3482
|
-
if (key in config) {
|
|
3483
|
-
return JSON.stringify(config[key], null, 2);
|
|
3531
|
+
async function detectRepo(cwd = process.cwd()) {
|
|
3532
|
+
const root = findGitRoot(cwd);
|
|
3533
|
+
if (!root) {
|
|
3534
|
+
throw new Error("Not inside a git repository. Run from a git repo or use `git init`.");
|
|
3484
3535
|
}
|
|
3485
|
-
|
|
3536
|
+
const remote_url = await parseRemoteUrl(root);
|
|
3537
|
+
const name = basename(root);
|
|
3538
|
+
return { root_path: root, name, remote_url };
|
|
3486
3539
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3540
|
+
|
|
3541
|
+
// packages/cli/src/util/ensure-repo.ts
|
|
3542
|
+
async function ensureRepo() {
|
|
3543
|
+
const info = await detectRepo();
|
|
3544
|
+
const res = await post("/repo/register", {
|
|
3545
|
+
root_path: info.root_path,
|
|
3546
|
+
name: info.name,
|
|
3547
|
+
remote_url: info.remote_url
|
|
3548
|
+
});
|
|
3549
|
+
return { repo_id: res.repo_id, name: res.name, root_path: info.root_path };
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// packages/cli/src/commands/context.ts
|
|
3553
|
+
async function contextCommand(goal, opts) {
|
|
3554
|
+
const { repo_id } = await ensureRepo();
|
|
3555
|
+
const res = await post("/context", { repo_id, goal });
|
|
3556
|
+
if (opts.json) {
|
|
3557
|
+
output(res, true);
|
|
3497
3558
|
} else {
|
|
3498
|
-
|
|
3559
|
+
output(res.context_pack, false);
|
|
3499
3560
|
}
|
|
3500
|
-
await writeConfig(config);
|
|
3501
3561
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3562
|
+
|
|
3563
|
+
// packages/cli/src/commands/daemon-ctrl.ts
|
|
3564
|
+
import { execSync, spawn } from "child_process";
|
|
3565
|
+
import { existsSync as existsSync2, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3566
|
+
import { createRequire } from "module";
|
|
3567
|
+
import { homedir } from "os";
|
|
3568
|
+
import { dirname as dirname2, join } from "path";
|
|
3569
|
+
import { fileURLToPath } from "url";
|
|
3570
|
+
var LENS_DIR = join(homedir(), ".lens");
|
|
3571
|
+
var PID_FILE = join(LENS_DIR, "daemon.pid");
|
|
3572
|
+
var LOG_FILE = join(LENS_DIR, "daemon.log");
|
|
3573
|
+
var LAUNCH_AGENTS_DIR = join(homedir(), "Library", "LaunchAgents");
|
|
3574
|
+
var PLIST_PATH = join(LAUNCH_AGENTS_DIR, "com.lens.daemon.plist");
|
|
3575
|
+
var SYSTEMD_DIR = join(homedir(), ".config", "systemd", "user");
|
|
3576
|
+
var SERVICE_PATH = join(SYSTEMD_DIR, "lens-daemon.service");
|
|
3577
|
+
var WIN_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
3578
|
+
var WIN_REG_NAME = "LensDaemon";
|
|
3579
|
+
function isDaemonRunning() {
|
|
3580
|
+
if (!existsSync2(PID_FILE)) return { running: false };
|
|
3581
|
+
const pid = Number(readFileSync(PID_FILE, "utf-8").trim());
|
|
3504
3582
|
try {
|
|
3505
|
-
|
|
3506
|
-
|
|
3583
|
+
process.kill(pid, 0);
|
|
3584
|
+
return { running: true, pid };
|
|
3507
3585
|
} catch {
|
|
3586
|
+
unlinkSync(PID_FILE);
|
|
3587
|
+
return { running: false };
|
|
3508
3588
|
}
|
|
3509
|
-
return "https://cloud.lens-engine.com";
|
|
3510
3589
|
}
|
|
3511
|
-
function
|
|
3590
|
+
function setupDarwin(daemonScript) {
|
|
3591
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3592
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3593
|
+
<plist version="1.0">
|
|
3594
|
+
<dict>
|
|
3595
|
+
<key>Label</key>
|
|
3596
|
+
<string>com.lens.daemon</string>
|
|
3597
|
+
<key>ProgramArguments</key>
|
|
3598
|
+
<array>
|
|
3599
|
+
<string>${process.execPath}</string>
|
|
3600
|
+
<string>${daemonScript}</string>
|
|
3601
|
+
</array>
|
|
3602
|
+
<key>RunAtLoad</key>
|
|
3603
|
+
<true/>
|
|
3604
|
+
<key>KeepAlive</key>
|
|
3605
|
+
<false/>
|
|
3606
|
+
<key>StandardOutPath</key>
|
|
3607
|
+
<string>${LOG_FILE}</string>
|
|
3608
|
+
<key>StandardErrorPath</key>
|
|
3609
|
+
<string>${LOG_FILE}</string>
|
|
3610
|
+
<key>EnvironmentVariables</key>
|
|
3611
|
+
<dict>
|
|
3612
|
+
<key>LENS_DAEMON</key>
|
|
3613
|
+
<string>1</string>
|
|
3614
|
+
</dict>
|
|
3615
|
+
</dict>
|
|
3616
|
+
</plist>
|
|
3617
|
+
`;
|
|
3618
|
+
const existing = existsSync2(PLIST_PATH) ? readFileSync(PLIST_PATH, "utf-8") : "";
|
|
3619
|
+
if (existing === plist) return;
|
|
3620
|
+
if (existing) {
|
|
3621
|
+
try {
|
|
3622
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
3623
|
+
} catch {
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true });
|
|
3627
|
+
writeFileSync(PLIST_PATH, plist);
|
|
3512
3628
|
try {
|
|
3513
|
-
|
|
3629
|
+
execSync(`launchctl load "${PLIST_PATH}" 2>/dev/null`);
|
|
3514
3630
|
} catch {
|
|
3515
|
-
return DEFAULTS;
|
|
3516
3631
|
}
|
|
3517
3632
|
}
|
|
3518
|
-
function
|
|
3519
|
-
|
|
3520
|
-
|
|
3633
|
+
function teardownDarwin() {
|
|
3634
|
+
if (!existsSync2(PLIST_PATH)) return;
|
|
3635
|
+
try {
|
|
3636
|
+
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
3637
|
+
} catch {
|
|
3638
|
+
}
|
|
3521
3639
|
}
|
|
3640
|
+
function setupLinux(daemonScript) {
|
|
3641
|
+
const unit = `[Unit]
|
|
3642
|
+
Description=LENS Daemon
|
|
3522
3643
|
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3644
|
+
[Service]
|
|
3645
|
+
ExecStart=${process.execPath} ${daemonScript}
|
|
3646
|
+
Restart=no
|
|
3647
|
+
Environment=LENS_DAEMON=1
|
|
3648
|
+
StandardOutput=append:${LOG_FILE}
|
|
3649
|
+
StandardError=append:${LOG_FILE}
|
|
3650
|
+
|
|
3651
|
+
[Install]
|
|
3652
|
+
WantedBy=default.target
|
|
3653
|
+
`;
|
|
3654
|
+
const existing = existsSync2(SERVICE_PATH) ? readFileSync(SERVICE_PATH, "utf-8") : "";
|
|
3655
|
+
if (existing === unit) return;
|
|
3656
|
+
mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
3657
|
+
writeFileSync(SERVICE_PATH, unit);
|
|
3658
|
+
try {
|
|
3659
|
+
execSync("systemctl --user daemon-reload 2>/dev/null");
|
|
3660
|
+
} catch {
|
|
3535
3661
|
}
|
|
3536
|
-
return null;
|
|
3537
|
-
}
|
|
3538
|
-
async function injectClaudeMd(repoRoot) {
|
|
3539
|
-
const existingFile = await findTargetFile(repoRoot);
|
|
3540
|
-
const targetFile = existingFile ?? path2.join(repoRoot, "CLAUDE.md");
|
|
3541
|
-
let existingContent = "";
|
|
3542
3662
|
try {
|
|
3543
|
-
|
|
3544
|
-
if (existingContent.toLowerCase().includes(RLM_MARKER)) {
|
|
3545
|
-
return;
|
|
3546
|
-
}
|
|
3663
|
+
execSync("systemctl --user enable lens-daemon.service 2>/dev/null");
|
|
3547
3664
|
} catch {
|
|
3548
3665
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3666
|
+
}
|
|
3667
|
+
function teardownLinux() {
|
|
3668
|
+
try {
|
|
3669
|
+
execSync("systemctl --user disable lens-daemon.service 2>/dev/null");
|
|
3670
|
+
} catch {
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
function setupWindows(daemonScript) {
|
|
3674
|
+
const cmd = `"${process.execPath}" "${daemonScript}"`;
|
|
3675
|
+
try {
|
|
3676
|
+
const existing = execSync(`reg query "${WIN_REG_KEY}" /v ${WIN_REG_NAME} 2>nul`, { encoding: "utf-8" });
|
|
3677
|
+
if (existing.includes(cmd)) return;
|
|
3678
|
+
} catch {
|
|
3679
|
+
}
|
|
3680
|
+
try {
|
|
3681
|
+
execSync(`reg add "${WIN_REG_KEY}" /v ${WIN_REG_NAME} /t REG_SZ /d "${cmd}" /f 2>nul`);
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
function teardownWindows() {
|
|
3686
|
+
try {
|
|
3687
|
+
execSync(`reg delete "${WIN_REG_KEY}" /v ${WIN_REG_NAME} /f 2>nul`);
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
function setupAutoStart(daemonScript) {
|
|
3692
|
+
switch (process.platform) {
|
|
3693
|
+
case "darwin":
|
|
3694
|
+
setupDarwin(daemonScript);
|
|
3695
|
+
break;
|
|
3696
|
+
case "linux":
|
|
3697
|
+
setupLinux(daemonScript);
|
|
3698
|
+
break;
|
|
3699
|
+
case "win32":
|
|
3700
|
+
setupWindows(daemonScript);
|
|
3701
|
+
break;
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
function removeAutoStart() {
|
|
3705
|
+
switch (process.platform) {
|
|
3706
|
+
case "darwin":
|
|
3707
|
+
teardownDarwin();
|
|
3708
|
+
break;
|
|
3709
|
+
case "linux":
|
|
3710
|
+
teardownLinux();
|
|
3711
|
+
break;
|
|
3712
|
+
case "win32":
|
|
3713
|
+
teardownWindows();
|
|
3714
|
+
break;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
async function startCommand() {
|
|
3718
|
+
const status = isDaemonRunning();
|
|
3719
|
+
if (status.running) {
|
|
3720
|
+
output(`LENS daemon already running (pid: ${status.pid})`, false);
|
|
3551
3721
|
return;
|
|
3552
3722
|
}
|
|
3553
|
-
|
|
3723
|
+
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
3724
|
+
const sibling = join(selfDir, "daemon.js");
|
|
3725
|
+
let daemonScript;
|
|
3726
|
+
if (existsSync2(sibling)) {
|
|
3727
|
+
daemonScript = sibling;
|
|
3728
|
+
} else {
|
|
3729
|
+
try {
|
|
3730
|
+
const require2 = createRequire(import.meta.url);
|
|
3731
|
+
daemonScript = require2.resolve("@lens/daemon");
|
|
3732
|
+
} catch {
|
|
3733
|
+
error("Could not find @lens/daemon. Run `pnpm build` first.");
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
mkdirSync(LENS_DIR, { recursive: true });
|
|
3738
|
+
setupAutoStart(daemonScript);
|
|
3739
|
+
if (isDaemonRunning().running) {
|
|
3740
|
+
const check2 = isDaemonRunning();
|
|
3741
|
+
output(`LENS daemon started (pid: ${check2.pid})`, false);
|
|
3554
3742
|
return;
|
|
3555
3743
|
}
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3744
|
+
const logFd = openSync(LOG_FILE, "a");
|
|
3745
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
3746
|
+
detached: true,
|
|
3747
|
+
stdio: ["ignore", logFd, logFd],
|
|
3748
|
+
env: { ...process.env, LENS_DAEMON: "1" }
|
|
3749
|
+
});
|
|
3750
|
+
child.unref();
|
|
3751
|
+
for (let i = 0; i < 20; i++) {
|
|
3752
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
3753
|
+
if (isDaemonRunning().running) break;
|
|
3754
|
+
}
|
|
3755
|
+
const check = isDaemonRunning();
|
|
3756
|
+
if (check.running) {
|
|
3757
|
+
output(`LENS daemon started (pid: ${check.pid})`, false);
|
|
3561
3758
|
} else {
|
|
3562
|
-
|
|
3759
|
+
error(`LENS daemon failed to start. Check logs: ${LOG_FILE}`);
|
|
3563
3760
|
}
|
|
3564
3761
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
existing = JSON.parse(readFileSync(mcpPath, "utf-8"));
|
|
3579
|
-
} catch {
|
|
3762
|
+
async function stopCommand() {
|
|
3763
|
+
const status = isDaemonRunning();
|
|
3764
|
+
if (!status.running) {
|
|
3765
|
+
output("LENS daemon is not running", false);
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
removeAutoStart();
|
|
3769
|
+
process.kill(status.pid, "SIGTERM");
|
|
3770
|
+
for (let i = 0; i < 20; i++) {
|
|
3771
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
3772
|
+
if (!isDaemonRunning().running) {
|
|
3773
|
+
output(`LENS daemon stopped (pid: ${status.pid})`, false);
|
|
3774
|
+
return;
|
|
3580
3775
|
}
|
|
3581
3776
|
}
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3777
|
+
error(`Daemon did not stop within 5s. Force kill with: kill -9 ${status.pid}`);
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// packages/cli/src/commands/daemon-stats.ts
|
|
3781
|
+
async function daemonStatsCommand(opts) {
|
|
3782
|
+
const s = await get("/daemon/stats");
|
|
3783
|
+
if (opts.json) {
|
|
3784
|
+
output(s, true);
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
const embedPct = s.total_chunks > 0 ? Math.round(s.total_embeddings / s.total_chunks * 100) : 0;
|
|
3788
|
+
const maintResult = s.last_maintenance_result ? `${s.last_maintenance_result.processed} processed, ${s.last_maintenance_result.errors} errors` : "\u2014";
|
|
3789
|
+
const lines = [
|
|
3790
|
+
"## LENS Daemon",
|
|
3791
|
+
"",
|
|
3792
|
+
` Repos: ${s.repos_count}`,
|
|
3793
|
+
` Chunks: ${s.total_chunks.toLocaleString()}`,
|
|
3794
|
+
` Embeddings: ${s.total_embeddings.toLocaleString()} (${embedPct}%)`,
|
|
3795
|
+
` DB Size: ${s.db_size_mb} MB`,
|
|
3796
|
+
"",
|
|
3797
|
+
" ## Maintenance Cron",
|
|
3798
|
+
` Last maintenance: ${s.last_maintenance_at ?? "never"}`,
|
|
3799
|
+
` Next maintenance: ${s.next_maintenance_at ?? "\u2014"}`,
|
|
3800
|
+
` Last result: ${maintResult}`
|
|
3801
|
+
];
|
|
3802
|
+
output(lines.join("\n"), false);
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
// packages/cli/src/commands/dashboard.ts
|
|
3806
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
3807
|
+
import { homedir as homedir2 } from "os";
|
|
3808
|
+
import { join as join2 } from "path";
|
|
3809
|
+
|
|
3810
|
+
// packages/cli/src/util/browser.ts
|
|
3811
|
+
import { exec } from "child_process";
|
|
3812
|
+
function openBrowser(url) {
|
|
3813
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3814
|
+
exec(`${cmd} ${url}`);
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
// packages/cli/src/commands/dashboard.ts
|
|
3818
|
+
var PID_FILE2 = join2(homedir2(), ".lens", "daemon.pid");
|
|
3819
|
+
function isDaemonRunning2() {
|
|
3820
|
+
if (!existsSync3(PID_FILE2)) return false;
|
|
3821
|
+
const pid = Number(readFileSync2(PID_FILE2, "utf-8").trim());
|
|
3822
|
+
try {
|
|
3823
|
+
process.kill(pid, 0);
|
|
3824
|
+
return true;
|
|
3825
|
+
} catch {
|
|
3826
|
+
return false;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
async function dashboardCommand() {
|
|
3830
|
+
if (!isDaemonRunning2()) {
|
|
3831
|
+
error("LENS daemon is not running. Start with: lens daemon start");
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
const port = process.env.LENS_PORT || "4111";
|
|
3835
|
+
const host = process.env.LENS_HOST || `http://127.0.0.1:${port}`;
|
|
3836
|
+
const url = `${host}/dashboard`;
|
|
3837
|
+
output(`Opening ${url}`, false);
|
|
3838
|
+
openBrowser(url);
|
|
3590
3839
|
}
|
|
3591
3840
|
|
|
3592
3841
|
// packages/cli/src/util/progress.ts
|
|
@@ -3618,7 +3867,7 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
|
|
|
3618
3867
|
if (connFailures >= MAX_CONN_FAILURES) {
|
|
3619
3868
|
process.stdout.write(
|
|
3620
3869
|
`
|
|
3621
|
-
${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim(
|
|
3870
|
+
${red("\u2717")} ${bold("Daemon unavailable")} \u2014 ${dim(`connection lost after ${connFailures} retries`)}
|
|
3622
3871
|
`
|
|
3623
3872
|
);
|
|
3624
3873
|
process.stdout.write(` ${dim("Restart with: lens start")}
|
|
@@ -3693,7 +3942,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
|
|
|
3693
3942
|
if (embDone) {
|
|
3694
3943
|
lines.push(` ${green("\u2713")} Embeddings ${dim(`${s.embedded_count}/${s.embeddable_count} code chunks`)}`);
|
|
3695
3944
|
} else if (s.embedding_quota_exceeded) {
|
|
3696
|
-
lines.push(
|
|
3945
|
+
lines.push(
|
|
3946
|
+
` ${yellow("\u26A0")} Embeddings ${dim(`quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}`)}`
|
|
3947
|
+
);
|
|
3697
3948
|
} else if (s.embeddable_count === 0) {
|
|
3698
3949
|
lines.push(` ${dim("\u25CB")} Embeddings ${dim("no code chunks")}`);
|
|
3699
3950
|
} else {
|
|
@@ -3705,7 +3956,9 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
|
|
|
3705
3956
|
if (sumDone) {
|
|
3706
3957
|
lines.push(` ${green("\u2713")} Summaries ${dim(`${s.purpose_count}/${s.purpose_total} files`)}`);
|
|
3707
3958
|
} else if (s.purpose_quota_exceeded) {
|
|
3708
|
-
lines.push(
|
|
3959
|
+
lines.push(
|
|
3960
|
+
` ${yellow("\u26A0")} Summaries ${dim(`quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}`)}`
|
|
3961
|
+
);
|
|
3709
3962
|
} else if (s.purpose_total > 0) {
|
|
3710
3963
|
lines.push(
|
|
3711
3964
|
` ${cyan(f)} Summaries ${createBar(Math.round(s.purpose_count / s.purpose_total * 100), 20)} ${dim(`${s.purpose_count}/${s.purpose_total}`)}`
|
|
@@ -3754,7 +4007,8 @@ async function showProgress(repoId, name, timeoutMs = 18e5) {
|
|
|
3754
4007
|
if (lastRendered) {
|
|
3755
4008
|
process.stdout.write(`\x1B[${TOTAL_LINES}A\x1B[J`);
|
|
3756
4009
|
}
|
|
3757
|
-
process.stdout.write(output2
|
|
4010
|
+
process.stdout.write(`${output2}
|
|
4011
|
+
`);
|
|
3758
4012
|
lastRendered = output2;
|
|
3759
4013
|
if (allDone) {
|
|
3760
4014
|
done = true;
|
|
@@ -3774,65 +4028,6 @@ function sleep2(ms) {
|
|
|
3774
4028
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3775
4029
|
}
|
|
3776
4030
|
|
|
3777
|
-
// packages/cli/src/commands/register.ts
|
|
3778
|
-
async function registerCommand(opts) {
|
|
3779
|
-
const info = await detectRepo();
|
|
3780
|
-
const res = await post("/repo/register", {
|
|
3781
|
-
root_path: info.root_path,
|
|
3782
|
-
name: info.name,
|
|
3783
|
-
remote_url: info.remote_url
|
|
3784
|
-
});
|
|
3785
|
-
if (opts.json) {
|
|
3786
|
-
output(res, true);
|
|
3787
|
-
return;
|
|
3788
|
-
}
|
|
3789
|
-
const mcpResult = injectMcp(info.root_path);
|
|
3790
|
-
if (res.created) {
|
|
3791
|
-
await injectClaudeMd(info.root_path);
|
|
3792
|
-
output(`Registered ${res.name} (repo_id: ${res.repo_id})`, false);
|
|
3793
|
-
if (mcpResult === "created" || mcpResult === "updated") {
|
|
3794
|
-
output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
|
|
3795
|
-
}
|
|
3796
|
-
const config = await readConfig();
|
|
3797
|
-
if (config.show_progress) {
|
|
3798
|
-
await showProgress(res.repo_id, res.name);
|
|
3799
|
-
} else {
|
|
3800
|
-
output(`Indexing started. Run \`lens status\` to check progress.`, false);
|
|
3801
|
-
}
|
|
3802
|
-
} else {
|
|
3803
|
-
output(`Already registered ${res.name} (repo_id: ${res.repo_id})`, false);
|
|
3804
|
-
if (mcpResult === "created" || mcpResult === "updated") {
|
|
3805
|
-
output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
|
|
3806
|
-
}
|
|
3807
|
-
if (opts.inject) {
|
|
3808
|
-
await injectClaudeMd(info.root_path);
|
|
3809
|
-
output(`Injected LENS instructions into CLAUDE.md`, false);
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
// packages/cli/src/util/ensure-repo.ts
|
|
3815
|
-
async function ensureRepo() {
|
|
3816
|
-
const info = await detectRepo();
|
|
3817
|
-
const res = await post("/repo/register", {
|
|
3818
|
-
root_path: info.root_path,
|
|
3819
|
-
name: info.name,
|
|
3820
|
-
remote_url: info.remote_url
|
|
3821
|
-
});
|
|
3822
|
-
return { repo_id: res.repo_id, name: res.name, root_path: info.root_path };
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
// packages/cli/src/commands/context.ts
|
|
3826
|
-
async function contextCommand(goal, opts) {
|
|
3827
|
-
const { repo_id } = await ensureRepo();
|
|
3828
|
-
const res = await post("/context", { repo_id, goal });
|
|
3829
|
-
if (opts.json) {
|
|
3830
|
-
output(res, true);
|
|
3831
|
-
} else {
|
|
3832
|
-
output(res.context_pack, false);
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
|
|
3836
4031
|
// packages/cli/src/commands/index.ts
|
|
3837
4032
|
async function indexCommand(opts) {
|
|
3838
4033
|
const { repo_id, name } = await ensureRepo();
|
|
@@ -3866,56 +4061,6 @@ async function indexCommand(opts) {
|
|
|
3866
4061
|
await showProgress(repo_id, name);
|
|
3867
4062
|
}
|
|
3868
4063
|
|
|
3869
|
-
// packages/cli/src/commands/status.ts
|
|
3870
|
-
var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
3871
|
-
var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
3872
|
-
var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
3873
|
-
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
3874
|
-
async function statusCommand(opts) {
|
|
3875
|
-
const { repo_id, name } = await ensureRepo();
|
|
3876
|
-
const s = await get(`/repo/${repo_id}/status`);
|
|
3877
|
-
if (opts.json) {
|
|
3878
|
-
output(s, true);
|
|
3879
|
-
return;
|
|
3880
|
-
}
|
|
3881
|
-
const staleTag = s.is_stale ? yellow2(" STALE") : "";
|
|
3882
|
-
const check = green2("\u2713");
|
|
3883
|
-
const pending = dim2("\u25CB");
|
|
3884
|
-
const hasCaps = s.has_capabilities !== false;
|
|
3885
|
-
const lines = [
|
|
3886
|
-
``,
|
|
3887
|
-
` ${bold2(name)}${staleTag}`,
|
|
3888
|
-
dim2(` ${"\u2500".repeat(40)}`),
|
|
3889
|
-
` ${s.chunk_count > 0 ? check : pending} Chunks ${dim2(s.chunk_count.toLocaleString())}`,
|
|
3890
|
-
` ${s.metadata_count > 0 ? check : pending} Metadata ${dim2(`${s.metadata_count.toLocaleString()} files`)}`,
|
|
3891
|
-
` ${s.git_commits_analyzed > 0 ? check : pending} Git history ${dim2(`${s.git_commits_analyzed.toLocaleString()} files`)}`,
|
|
3892
|
-
` ${s.import_edge_count > 0 ? check : pending} Import graph ${dim2(`${s.import_edge_count.toLocaleString()} edges`)}`,
|
|
3893
|
-
` ${s.cochange_pairs > 0 ? check : pending} Co-changes ${dim2(`${s.cochange_pairs.toLocaleString()} pairs`)}`
|
|
3894
|
-
];
|
|
3895
|
-
if (hasCaps) {
|
|
3896
|
-
const warn = yellow2("\u26A0");
|
|
3897
|
-
const embDone = s.embedded_pct >= 100 || s.embedded_count >= s.embeddable_count && s.embeddable_count > 0;
|
|
3898
|
-
const embLabel = s.embedding_quota_exceeded ? `quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}` : s.embeddable_count > 0 ? `${s.embedded_count}/${s.embeddable_count} code chunks (${s.embedded_pct}%)` : "no code chunks";
|
|
3899
|
-
const embIcon = s.embedding_quota_exceeded ? warn : embDone ? check : pending;
|
|
3900
|
-
const vocabLabel = s.vocab_cluster_count > 0 ? `${s.vocab_cluster_count} clusters` : "...";
|
|
3901
|
-
const vocabIcon = s.vocab_cluster_count > 0 ? check : pending;
|
|
3902
|
-
const purDone = s.purpose_count > 0 && s.purpose_count >= s.purpose_total;
|
|
3903
|
-
const purposeLabel = s.purpose_quota_exceeded ? `quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}` : s.purpose_total > 0 ? `${s.purpose_count}/${s.purpose_total} files` : "no files";
|
|
3904
|
-
const purposeIcon = s.purpose_quota_exceeded ? warn : purDone ? check : pending;
|
|
3905
|
-
lines.push(` ${vocabIcon} Vocab clust. ${dim2(vocabLabel)}`);
|
|
3906
|
-
lines.push(` ${embIcon} Embeddings ${dim2(embLabel)}`);
|
|
3907
|
-
lines.push(` ${purposeIcon} Summaries ${dim2(purposeLabel)}`);
|
|
3908
|
-
} else {
|
|
3909
|
-
lines.push(``);
|
|
3910
|
-
lines.push(` ${yellow2("\u26A1")} ${bold2("Pro")}`);
|
|
3911
|
-
lines.push(` ${dim2("\xB7 Vocab clusters")}`);
|
|
3912
|
-
lines.push(` ${dim2("\xB7 Embeddings")}`);
|
|
3913
|
-
lines.push(` ${dim2("\xB7 Summaries")}`);
|
|
3914
|
-
lines.push(` ${dim2("lens login \u2192 upgrade to enable")}`);
|
|
3915
|
-
}
|
|
3916
|
-
output(lines.join("\n"), false);
|
|
3917
|
-
}
|
|
3918
|
-
|
|
3919
4064
|
// packages/cli/src/commands/list.ts
|
|
3920
4065
|
async function listCommand(opts) {
|
|
3921
4066
|
const res = await get("/repo/list/detailed");
|
|
@@ -3932,280 +4077,43 @@ async function listCommand(opts) {
|
|
|
3932
4077
|
const lines = [header, sep];
|
|
3933
4078
|
for (const r of res.repos) {
|
|
3934
4079
|
const id = r.id.slice(0, 8);
|
|
3935
|
-
const indexed = r.last_indexed_at ? timeAgo(r.last_indexed_at) : "never";
|
|
3936
|
-
lines.push(
|
|
3937
|
-
pad(id, 10) + pad(r.name, 20) + pad(r.index_status, 10) + pad(String(r.files_indexed), 8) + pad(r.chunk_count.toLocaleString(), 10) + pad(`${r.embedded_pct}%`, 8) + indexed
|
|
3938
|
-
);
|
|
3939
|
-
}
|
|
3940
|
-
output(lines.join("\n"), false);
|
|
3941
|
-
}
|
|
3942
|
-
function pad(s, width) {
|
|
3943
|
-
return s.length >= width ? s.slice(0, width - 1) + " " : s + " ".repeat(width - s.length);
|
|
3944
|
-
}
|
|
3945
|
-
function timeAgo(iso) {
|
|
3946
|
-
const diff = Date.now() - new Date(iso).getTime();
|
|
3947
|
-
const mins = Math.floor(diff / 6e4);
|
|
3948
|
-
if (mins < 1) return "just now";
|
|
3949
|
-
if (mins < 60) return `${mins}m ago`;
|
|
3950
|
-
const hours = Math.floor(mins / 60);
|
|
3951
|
-
if (hours < 24) return `${hours}h ago`;
|
|
3952
|
-
const days = Math.floor(hours / 24);
|
|
3953
|
-
return `${days}d ago`;
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
// packages/cli/src/commands/remove.ts
|
|
3957
|
-
async function removeCommand(opts) {
|
|
3958
|
-
const { repo_id, name } = await ensureRepo();
|
|
3959
|
-
if (!opts.yes) {
|
|
3960
|
-
process.stderr.write(`Remove "${name}" (${repo_id})? All index data will be deleted.
|
|
3961
|
-
`);
|
|
3962
|
-
process.stderr.write(`Re-run with --yes to confirm.
|
|
3963
|
-
`);
|
|
3964
|
-
process.exit(1);
|
|
3965
|
-
}
|
|
3966
|
-
const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3967
|
-
let i = 0;
|
|
3968
|
-
let phase = "Stopping watcher";
|
|
3969
|
-
const interval = setInterval(() => {
|
|
3970
|
-
const frame = spinner[i % spinner.length];
|
|
3971
|
-
process.stdout.write(`\r${frame} ${phase}...`);
|
|
3972
|
-
i++;
|
|
3973
|
-
}, 80);
|
|
3974
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
3975
|
-
phase = `Removing ${name}`;
|
|
3976
|
-
const res = await request("DELETE", `/repo/${repo_id}`);
|
|
3977
|
-
clearInterval(interval);
|
|
3978
|
-
if (opts.json) {
|
|
3979
|
-
output(res, true);
|
|
3980
|
-
} else {
|
|
3981
|
-
process.stdout.write(`\r\u2713 Removed ${name}: ${res.chunks_removed} chunks deleted
|
|
3982
|
-
`);
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
|
|
3986
|
-
// packages/cli/src/commands/daemon-stats.ts
|
|
3987
|
-
async function daemonStatsCommand(opts) {
|
|
3988
|
-
const s = await get("/daemon/stats");
|
|
3989
|
-
if (opts.json) {
|
|
3990
|
-
output(s, true);
|
|
3991
|
-
return;
|
|
3992
|
-
}
|
|
3993
|
-
const embedPct = s.total_chunks > 0 ? Math.round(s.total_embeddings / s.total_chunks * 100) : 0;
|
|
3994
|
-
const maintResult = s.last_maintenance_result ? `${s.last_maintenance_result.processed} processed, ${s.last_maintenance_result.errors} errors` : "\u2014";
|
|
3995
|
-
const lines = [
|
|
3996
|
-
"## LENS Daemon",
|
|
3997
|
-
"",
|
|
3998
|
-
` Repos: ${s.repos_count}`,
|
|
3999
|
-
` Chunks: ${s.total_chunks.toLocaleString()}`,
|
|
4000
|
-
` Embeddings: ${s.total_embeddings.toLocaleString()} (${embedPct}%)`,
|
|
4001
|
-
` DB Size: ${s.db_size_mb} MB`,
|
|
4002
|
-
"",
|
|
4003
|
-
" ## Maintenance Cron",
|
|
4004
|
-
` Last maintenance: ${s.last_maintenance_at ?? "never"}`,
|
|
4005
|
-
` Next maintenance: ${s.next_maintenance_at ?? "\u2014"}`,
|
|
4006
|
-
` Last result: ${maintResult}`
|
|
4007
|
-
];
|
|
4008
|
-
output(lines.join("\n"), false);
|
|
4009
|
-
}
|
|
4010
|
-
|
|
4011
|
-
// packages/cli/src/commands/watch.ts
|
|
4012
|
-
async function watchCommand(opts) {
|
|
4013
|
-
const { repo_id, name } = await ensureRepo();
|
|
4014
|
-
const res = await post("/index/watch", { repo_id });
|
|
4015
|
-
if (opts.json) {
|
|
4016
|
-
output(res, true);
|
|
4017
|
-
} else if (res.already_watching) {
|
|
4018
|
-
output(`${name} is already being watched`, false);
|
|
4019
|
-
} else {
|
|
4020
|
-
output(`File watcher started for ${name}`, false);
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
async function unwatchCommand(opts) {
|
|
4024
|
-
const { repo_id, name } = await ensureRepo();
|
|
4025
|
-
const res = await post("/index/unwatch", { repo_id });
|
|
4026
|
-
if (opts.json) {
|
|
4027
|
-
output(res, true);
|
|
4028
|
-
} else if (res.stopped) {
|
|
4029
|
-
output(`File watcher stopped for ${name}`, false);
|
|
4030
|
-
} else {
|
|
4031
|
-
output(`${name} was not being watched`, false);
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
async function watchStatusCommand(opts) {
|
|
4035
|
-
const { repo_id, name } = await ensureRepo();
|
|
4036
|
-
const res = await get(`/index/watch-status/${repo_id}`);
|
|
4037
|
-
if (opts.json) {
|
|
4038
|
-
output(res, true);
|
|
4039
|
-
} else if (res.watching) {
|
|
4040
|
-
output(
|
|
4041
|
-
`${name}: watching since ${res.started_at}
|
|
4042
|
-
Changed: ${res.changed_files} files | Deleted: ${res.deleted_files} files`,
|
|
4043
|
-
false
|
|
4044
|
-
);
|
|
4045
|
-
} else {
|
|
4046
|
-
output(`${name}: not watching. Run \`lens repo watch\` to start.`, false);
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
|
|
4050
|
-
// packages/cli/src/commands/mcp.ts
|
|
4051
|
-
async function mcpCommand() {
|
|
4052
|
-
const { root_path } = await detectRepo();
|
|
4053
|
-
const result = injectMcp(root_path);
|
|
4054
|
-
if (result === "exists") {
|
|
4055
|
-
output("LENS MCP entry already present in .mcp.json", false);
|
|
4056
|
-
} else {
|
|
4057
|
-
output("Wrote .mcp.json \u2014 agents will auto-discover LENS", false);
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
// packages/cli/src/commands/config.ts
|
|
4062
|
-
async function configGetCommand(key) {
|
|
4063
|
-
try {
|
|
4064
|
-
const value = await configGet(key);
|
|
4065
|
-
output(value, false);
|
|
4066
|
-
} catch (err) {
|
|
4067
|
-
output(err instanceof Error ? err.message : String(err), false);
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
async function configSetCommand(key, value) {
|
|
4071
|
-
try {
|
|
4072
|
-
await configSet(key, value);
|
|
4073
|
-
output(`Config updated: ${key} = ${value}`, false);
|
|
4074
|
-
} catch (err) {
|
|
4075
|
-
output(err instanceof Error ? err.message : String(err), false);
|
|
4076
|
-
process.exit(1);
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
|
|
4080
|
-
// packages/cli/src/commands/daemon-ctrl.ts
|
|
4081
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync, openSync, mkdirSync } from "fs";
|
|
4082
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
4083
|
-
import { homedir } from "os";
|
|
4084
|
-
import { spawn } from "child_process";
|
|
4085
|
-
import { fileURLToPath } from "url";
|
|
4086
|
-
import { createRequire } from "module";
|
|
4087
|
-
var LENS_DIR = join2(homedir(), ".lens");
|
|
4088
|
-
var PID_FILE = join2(LENS_DIR, "daemon.pid");
|
|
4089
|
-
var LOG_FILE = join2(LENS_DIR, "daemon.log");
|
|
4090
|
-
function isDaemonRunning() {
|
|
4091
|
-
if (!existsSync3(PID_FILE)) return { running: false };
|
|
4092
|
-
const pid = Number(readFileSync2(PID_FILE, "utf-8").trim());
|
|
4093
|
-
try {
|
|
4094
|
-
process.kill(pid, 0);
|
|
4095
|
-
return { running: true, pid };
|
|
4096
|
-
} catch {
|
|
4097
|
-
unlinkSync(PID_FILE);
|
|
4098
|
-
return { running: false };
|
|
4099
|
-
}
|
|
4100
|
-
}
|
|
4101
|
-
async function startCommand() {
|
|
4102
|
-
const status = isDaemonRunning();
|
|
4103
|
-
if (status.running) {
|
|
4104
|
-
output(`LENS daemon already running (pid: ${status.pid})`, false);
|
|
4105
|
-
return;
|
|
4106
|
-
}
|
|
4107
|
-
const selfDir = dirname2(fileURLToPath(import.meta.url));
|
|
4108
|
-
const sibling = join2(selfDir, "daemon.js");
|
|
4109
|
-
let daemonScript;
|
|
4110
|
-
if (existsSync3(sibling)) {
|
|
4111
|
-
daemonScript = sibling;
|
|
4112
|
-
} else {
|
|
4113
|
-
try {
|
|
4114
|
-
const require2 = createRequire(import.meta.url);
|
|
4115
|
-
daemonScript = require2.resolve("@lens/daemon");
|
|
4116
|
-
} catch {
|
|
4117
|
-
error("Could not find @lens/daemon. Run `pnpm build` first.");
|
|
4118
|
-
return;
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
mkdirSync(LENS_DIR, { recursive: true });
|
|
4122
|
-
const logFd = openSync(LOG_FILE, "a");
|
|
4123
|
-
const child = spawn(process.execPath, [daemonScript], {
|
|
4124
|
-
detached: true,
|
|
4125
|
-
stdio: ["ignore", logFd, logFd],
|
|
4126
|
-
env: { ...process.env, LENS_DAEMON: "1" }
|
|
4127
|
-
});
|
|
4128
|
-
child.unref();
|
|
4129
|
-
for (let i = 0; i < 20; i++) {
|
|
4130
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
4131
|
-
if (isDaemonRunning().running) break;
|
|
4132
|
-
}
|
|
4133
|
-
const check = isDaemonRunning();
|
|
4134
|
-
if (check.running) {
|
|
4135
|
-
output(`LENS daemon started (pid: ${check.pid})`, false);
|
|
4136
|
-
} else {
|
|
4137
|
-
error(`LENS daemon failed to start. Check logs: ${LOG_FILE}`);
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
async function stopCommand() {
|
|
4141
|
-
const status = isDaemonRunning();
|
|
4142
|
-
if (!status.running) {
|
|
4143
|
-
output("LENS daemon is not running", false);
|
|
4144
|
-
return;
|
|
4145
|
-
}
|
|
4146
|
-
process.kill(status.pid, "SIGTERM");
|
|
4147
|
-
for (let i = 0; i < 20; i++) {
|
|
4148
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
4149
|
-
if (!isDaemonRunning().running) {
|
|
4150
|
-
output(`LENS daemon stopped (pid: ${status.pid})`, false);
|
|
4151
|
-
return;
|
|
4152
|
-
}
|
|
4153
|
-
}
|
|
4154
|
-
error("Daemon did not stop within 5s. Force kill with: kill -9 " + status.pid);
|
|
4155
|
-
}
|
|
4156
|
-
|
|
4157
|
-
// packages/cli/src/commands/dashboard.ts
|
|
4158
|
-
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
4159
|
-
import { join as join3 } from "path";
|
|
4160
|
-
import { homedir as homedir2 } from "os";
|
|
4161
|
-
|
|
4162
|
-
// packages/cli/src/util/browser.ts
|
|
4163
|
-
import { exec } from "child_process";
|
|
4164
|
-
function openBrowser(url) {
|
|
4165
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4166
|
-
exec(`${cmd} ${url}`);
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
// packages/cli/src/commands/dashboard.ts
|
|
4170
|
-
var PID_FILE2 = join3(homedir2(), ".lens", "daemon.pid");
|
|
4171
|
-
function isDaemonRunning2() {
|
|
4172
|
-
if (!existsSync4(PID_FILE2)) return false;
|
|
4173
|
-
const pid = Number(readFileSync3(PID_FILE2, "utf-8").trim());
|
|
4174
|
-
try {
|
|
4175
|
-
process.kill(pid, 0);
|
|
4176
|
-
return true;
|
|
4177
|
-
} catch {
|
|
4178
|
-
return false;
|
|
4080
|
+
const indexed = r.last_indexed_at ? timeAgo(r.last_indexed_at) : "never";
|
|
4081
|
+
lines.push(
|
|
4082
|
+
pad(id, 10) + pad(r.name, 20) + pad(r.index_status, 10) + pad(String(r.files_indexed), 8) + pad(r.chunk_count.toLocaleString(), 10) + pad(`${r.embedded_pct}%`, 8) + indexed
|
|
4083
|
+
);
|
|
4179
4084
|
}
|
|
4085
|
+
output(lines.join("\n"), false);
|
|
4180
4086
|
}
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4087
|
+
function pad(s, width) {
|
|
4088
|
+
return s.length >= width ? `${s.slice(0, width - 1)} ` : s + " ".repeat(width - s.length);
|
|
4089
|
+
}
|
|
4090
|
+
function timeAgo(iso) {
|
|
4091
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
4092
|
+
const mins = Math.floor(diff / 6e4);
|
|
4093
|
+
if (mins < 1) return "just now";
|
|
4094
|
+
if (mins < 60) return `${mins}m ago`;
|
|
4095
|
+
const hours = Math.floor(mins / 60);
|
|
4096
|
+
if (hours < 24) return `${hours}h ago`;
|
|
4097
|
+
const days = Math.floor(hours / 24);
|
|
4098
|
+
return `${days}d ago`;
|
|
4191
4099
|
}
|
|
4192
4100
|
|
|
4193
4101
|
// packages/cli/src/commands/login.ts
|
|
4194
4102
|
import http from "http";
|
|
4195
4103
|
|
|
4196
4104
|
// packages/cli/src/util/auth.ts
|
|
4197
|
-
import
|
|
4198
|
-
import path3 from "path";
|
|
4105
|
+
import fs2 from "fs/promises";
|
|
4199
4106
|
import os2 from "os";
|
|
4200
|
-
|
|
4201
|
-
var
|
|
4107
|
+
import path2 from "path";
|
|
4108
|
+
var CONFIG_DIR2 = path2.join(os2.homedir(), ".lens");
|
|
4109
|
+
var AUTH_FILE = path2.join(CONFIG_DIR2, "auth.json");
|
|
4202
4110
|
async function writeAuth(tokens) {
|
|
4203
|
-
await
|
|
4204
|
-
await
|
|
4111
|
+
await fs2.mkdir(CONFIG_DIR2, { recursive: true });
|
|
4112
|
+
await fs2.writeFile(AUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
4205
4113
|
}
|
|
4206
4114
|
async function clearAuth() {
|
|
4207
4115
|
try {
|
|
4208
|
-
await
|
|
4116
|
+
await fs2.unlink(AUTH_FILE);
|
|
4209
4117
|
} catch {
|
|
4210
4118
|
}
|
|
4211
4119
|
}
|
|
@@ -4560,12 +4468,19 @@ async function loginCommand(opts) {
|
|
|
4560
4468
|
}
|
|
4561
4469
|
if (req.method === "POST" && url.pathname === "/token") {
|
|
4562
4470
|
let body = "";
|
|
4563
|
-
req.on("data", (c) =>
|
|
4471
|
+
req.on("data", (c) => {
|
|
4472
|
+
body += c;
|
|
4473
|
+
});
|
|
4564
4474
|
req.on("end", async () => {
|
|
4565
4475
|
try {
|
|
4566
4476
|
const { access_token, refresh_token, expires_in, user_email } = JSON.parse(body);
|
|
4567
4477
|
const expires_at = Math.floor(Date.now() / 1e3) + Number(expires_in || 3600);
|
|
4568
|
-
const tokens = {
|
|
4478
|
+
const tokens = {
|
|
4479
|
+
access_token,
|
|
4480
|
+
refresh_token,
|
|
4481
|
+
user_email,
|
|
4482
|
+
expires_at
|
|
4483
|
+
};
|
|
4569
4484
|
try {
|
|
4570
4485
|
const keyRes = await fetch(`${CLOUD_API_URL}/auth/key`, {
|
|
4571
4486
|
headers: { Authorization: `Bearer ${access_token}` }
|
|
@@ -4591,7 +4506,7 @@ async function loginCommand(opts) {
|
|
|
4591
4506
|
cleanup();
|
|
4592
4507
|
resolve2();
|
|
4593
4508
|
setTimeout(() => process.exit(0), 100);
|
|
4594
|
-
} catch (
|
|
4509
|
+
} catch (_err) {
|
|
4595
4510
|
res.writeHead(400);
|
|
4596
4511
|
res.end("Bad request");
|
|
4597
4512
|
}
|
|
@@ -4638,8 +4553,245 @@ async function logoutCommand() {
|
|
|
4638
4553
|
output("Logged out of LENS cloud.", false);
|
|
4639
4554
|
}
|
|
4640
4555
|
|
|
4556
|
+
// packages/cli/src/util/inject-mcp.ts
|
|
4557
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4558
|
+
import { join as join3 } from "path";
|
|
4559
|
+
var MCP_ENTRY = {
|
|
4560
|
+
command: "npx",
|
|
4561
|
+
args: ["lens-daemon", "--stdio"]
|
|
4562
|
+
};
|
|
4563
|
+
function injectMcp(repoRoot) {
|
|
4564
|
+
const mcpPath = join3(repoRoot, ".mcp.json");
|
|
4565
|
+
let existing = {};
|
|
4566
|
+
if (existsSync4(mcpPath)) {
|
|
4567
|
+
try {
|
|
4568
|
+
existing = JSON.parse(readFileSync3(mcpPath, "utf-8"));
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
4573
|
+
const servers = existing.mcpServers;
|
|
4574
|
+
if (servers.lens) return "exists";
|
|
4575
|
+
const isNew = !existsSync4(mcpPath);
|
|
4576
|
+
servers.lens = MCP_ENTRY;
|
|
4577
|
+
writeFileSync2(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
4578
|
+
`);
|
|
4579
|
+
return isNew ? "created" : "updated";
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
// packages/cli/src/commands/mcp.ts
|
|
4583
|
+
async function mcpCommand() {
|
|
4584
|
+
const { root_path } = await detectRepo();
|
|
4585
|
+
const result = injectMcp(root_path);
|
|
4586
|
+
if (result === "exists") {
|
|
4587
|
+
output("LENS MCP entry already present in .mcp.json", false);
|
|
4588
|
+
} else {
|
|
4589
|
+
output("Wrote .mcp.json \u2014 agents will auto-discover LENS", false);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
// packages/cli/src/util/inject-claude-md.ts
|
|
4594
|
+
import fs3 from "fs/promises";
|
|
4595
|
+
import path3 from "path";
|
|
4596
|
+
var RLM_MARKER = "lens context";
|
|
4597
|
+
var CANDIDATE_PATHS = ["CLAUDE.md", "agents.md", "AGENTS.md", ".CLAUDE.md", ".agents.md", ".AGENTS.md"];
|
|
4598
|
+
async function findTargetFile(repoRoot) {
|
|
4599
|
+
for (const relPath of CANDIDATE_PATHS) {
|
|
4600
|
+
const fullPath = path3.join(repoRoot, relPath);
|
|
4601
|
+
try {
|
|
4602
|
+
await fs3.access(fullPath);
|
|
4603
|
+
return fullPath;
|
|
4604
|
+
} catch {
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
return null;
|
|
4608
|
+
}
|
|
4609
|
+
async function injectClaudeMd(repoRoot) {
|
|
4610
|
+
const existingFile = await findTargetFile(repoRoot);
|
|
4611
|
+
const targetFile = existingFile ?? path3.join(repoRoot, "CLAUDE.md");
|
|
4612
|
+
let existingContent = "";
|
|
4613
|
+
try {
|
|
4614
|
+
existingContent = await fs3.readFile(targetFile, "utf-8");
|
|
4615
|
+
if (existingContent.toLowerCase().includes(RLM_MARKER)) {
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
} catch {
|
|
4619
|
+
}
|
|
4620
|
+
const config = await readConfig();
|
|
4621
|
+
if (existingFile && config.inject_behavior === "once") {
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
if (config.inject_behavior === "skip") {
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
const { content } = await get("/repo/template");
|
|
4628
|
+
if (existingContent) {
|
|
4629
|
+
await fs3.writeFile(targetFile, `${content}
|
|
4630
|
+
|
|
4631
|
+
${existingContent}`);
|
|
4632
|
+
} else {
|
|
4633
|
+
await fs3.writeFile(targetFile, content);
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
// packages/cli/src/commands/register.ts
|
|
4638
|
+
async function registerCommand(opts) {
|
|
4639
|
+
const info = await detectRepo();
|
|
4640
|
+
const res = await post("/repo/register", {
|
|
4641
|
+
root_path: info.root_path,
|
|
4642
|
+
name: info.name,
|
|
4643
|
+
remote_url: info.remote_url
|
|
4644
|
+
});
|
|
4645
|
+
if (opts.json) {
|
|
4646
|
+
output(res, true);
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
const mcpResult = injectMcp(info.root_path);
|
|
4650
|
+
if (res.created) {
|
|
4651
|
+
await injectClaudeMd(info.root_path);
|
|
4652
|
+
output(`Registered ${res.name} (repo_id: ${res.repo_id})`, false);
|
|
4653
|
+
if (mcpResult === "created" || mcpResult === "updated") {
|
|
4654
|
+
output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
|
|
4655
|
+
}
|
|
4656
|
+
const config = await readConfig();
|
|
4657
|
+
if (config.show_progress) {
|
|
4658
|
+
await showProgress(res.repo_id, res.name);
|
|
4659
|
+
} else {
|
|
4660
|
+
output(`Indexing started. Run \`lens status\` to check progress.`, false);
|
|
4661
|
+
}
|
|
4662
|
+
} else {
|
|
4663
|
+
output(`Already registered ${res.name} (repo_id: ${res.repo_id})`, false);
|
|
4664
|
+
if (mcpResult === "created" || mcpResult === "updated") {
|
|
4665
|
+
output(`Wrote .mcp.json \u2192 Claude Code will auto-discover LENS`, false);
|
|
4666
|
+
}
|
|
4667
|
+
if (opts.inject) {
|
|
4668
|
+
await injectClaudeMd(info.root_path);
|
|
4669
|
+
output(`Injected LENS instructions into CLAUDE.md`, false);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
// packages/cli/src/commands/remove.ts
|
|
4675
|
+
async function removeCommand(opts) {
|
|
4676
|
+
const { repo_id, name } = await ensureRepo();
|
|
4677
|
+
if (!opts.yes) {
|
|
4678
|
+
process.stderr.write(`Remove "${name}" (${repo_id})? All index data will be deleted.
|
|
4679
|
+
`);
|
|
4680
|
+
process.stderr.write(`Re-run with --yes to confirm.
|
|
4681
|
+
`);
|
|
4682
|
+
process.exit(1);
|
|
4683
|
+
}
|
|
4684
|
+
const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4685
|
+
let i = 0;
|
|
4686
|
+
let phase = "Stopping watcher";
|
|
4687
|
+
const interval = setInterval(() => {
|
|
4688
|
+
const frame = spinner[i % spinner.length];
|
|
4689
|
+
process.stdout.write(`\r${frame} ${phase}...`);
|
|
4690
|
+
i++;
|
|
4691
|
+
}, 80);
|
|
4692
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
4693
|
+
phase = `Removing ${name}`;
|
|
4694
|
+
const res = await request("DELETE", `/repo/${repo_id}`);
|
|
4695
|
+
clearInterval(interval);
|
|
4696
|
+
if (opts.json) {
|
|
4697
|
+
output(res, true);
|
|
4698
|
+
} else {
|
|
4699
|
+
process.stdout.write(`\r\u2713 Removed ${name}: ${res.chunks_removed} chunks deleted
|
|
4700
|
+
`);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
// packages/cli/src/commands/status.ts
|
|
4705
|
+
var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
4706
|
+
var green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
4707
|
+
var yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
4708
|
+
var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
4709
|
+
async function statusCommand(opts) {
|
|
4710
|
+
const { repo_id, name } = await ensureRepo();
|
|
4711
|
+
const s = await get(`/repo/${repo_id}/status`);
|
|
4712
|
+
if (opts.json) {
|
|
4713
|
+
output(s, true);
|
|
4714
|
+
return;
|
|
4715
|
+
}
|
|
4716
|
+
const staleTag = s.is_stale ? yellow2(" STALE") : "";
|
|
4717
|
+
const check = green2("\u2713");
|
|
4718
|
+
const pending = dim2("\u25CB");
|
|
4719
|
+
const hasCaps = s.has_capabilities !== false;
|
|
4720
|
+
const lines = [
|
|
4721
|
+
``,
|
|
4722
|
+
` ${bold2(name)}${staleTag}`,
|
|
4723
|
+
dim2(` ${"\u2500".repeat(40)}`),
|
|
4724
|
+
` ${s.chunk_count > 0 ? check : pending} Chunks ${dim2(s.chunk_count.toLocaleString())}`,
|
|
4725
|
+
` ${s.metadata_count > 0 ? check : pending} Metadata ${dim2(`${s.metadata_count.toLocaleString()} files`)}`,
|
|
4726
|
+
` ${s.git_commits_analyzed > 0 ? check : pending} Git history ${dim2(`${s.git_commits_analyzed.toLocaleString()} files`)}`,
|
|
4727
|
+
` ${s.import_edge_count > 0 ? check : pending} Import graph ${dim2(`${s.import_edge_count.toLocaleString()} edges`)}`,
|
|
4728
|
+
` ${s.cochange_pairs > 0 ? check : pending} Co-changes ${dim2(`${s.cochange_pairs.toLocaleString()} pairs`)}`
|
|
4729
|
+
];
|
|
4730
|
+
if (hasCaps) {
|
|
4731
|
+
const warn = yellow2("\u26A0");
|
|
4732
|
+
const embDone = s.embedded_pct >= 100 || s.embedded_count >= s.embeddable_count && s.embeddable_count > 0;
|
|
4733
|
+
const embLabel = s.embedding_quota_exceeded ? `quota exceeded \u2014 ${s.embedded_count}/${s.embeddable_count}` : s.embeddable_count > 0 ? `${s.embedded_count}/${s.embeddable_count} code chunks (${s.embedded_pct}%)` : "no code chunks";
|
|
4734
|
+
const embIcon = s.embedding_quota_exceeded ? warn : embDone ? check : pending;
|
|
4735
|
+
const vocabLabel = s.vocab_cluster_count > 0 ? `${s.vocab_cluster_count} clusters` : "...";
|
|
4736
|
+
const vocabIcon = s.vocab_cluster_count > 0 ? check : pending;
|
|
4737
|
+
const purDone = s.purpose_count > 0 && s.purpose_count >= s.purpose_total;
|
|
4738
|
+
const purposeLabel = s.purpose_quota_exceeded ? `quota exceeded \u2014 ${s.purpose_count}/${s.purpose_total}` : s.purpose_total > 0 ? `${s.purpose_count}/${s.purpose_total} files` : "no files";
|
|
4739
|
+
const purposeIcon = s.purpose_quota_exceeded ? warn : purDone ? check : pending;
|
|
4740
|
+
lines.push(` ${vocabIcon} Vocab clust. ${dim2(vocabLabel)}`);
|
|
4741
|
+
lines.push(` ${embIcon} Embeddings ${dim2(embLabel)}`);
|
|
4742
|
+
lines.push(` ${purposeIcon} Summaries ${dim2(purposeLabel)}`);
|
|
4743
|
+
} else {
|
|
4744
|
+
lines.push(``);
|
|
4745
|
+
lines.push(` ${yellow2("\u26A1")} ${bold2("Pro")}`);
|
|
4746
|
+
lines.push(` ${dim2("\xB7 Vocab clusters")}`);
|
|
4747
|
+
lines.push(` ${dim2("\xB7 Embeddings")}`);
|
|
4748
|
+
lines.push(` ${dim2("\xB7 Summaries")}`);
|
|
4749
|
+
lines.push(` ${dim2("lens login \u2192 upgrade to enable")}`);
|
|
4750
|
+
}
|
|
4751
|
+
output(lines.join("\n"), false);
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
// packages/cli/src/commands/watch.ts
|
|
4755
|
+
async function watchCommand(opts) {
|
|
4756
|
+
const { repo_id, name } = await ensureRepo();
|
|
4757
|
+
const res = await post("/index/watch", { repo_id });
|
|
4758
|
+
if (opts.json) {
|
|
4759
|
+
output(res, true);
|
|
4760
|
+
} else if (res.already_watching) {
|
|
4761
|
+
output(`${name} is already being watched`, false);
|
|
4762
|
+
} else {
|
|
4763
|
+
output(`File watcher started for ${name}`, false);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
async function unwatchCommand(opts) {
|
|
4767
|
+
const { repo_id, name } = await ensureRepo();
|
|
4768
|
+
const res = await post("/index/unwatch", { repo_id });
|
|
4769
|
+
if (opts.json) {
|
|
4770
|
+
output(res, true);
|
|
4771
|
+
} else if (res.stopped) {
|
|
4772
|
+
output(`File watcher stopped for ${name}`, false);
|
|
4773
|
+
} else {
|
|
4774
|
+
output(`${name} was not being watched`, false);
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
async function watchStatusCommand(opts) {
|
|
4778
|
+
const { repo_id, name } = await ensureRepo();
|
|
4779
|
+
const res = await get(`/index/watch-status/${repo_id}`);
|
|
4780
|
+
if (opts.json) {
|
|
4781
|
+
output(res, true);
|
|
4782
|
+
} else if (res.watching) {
|
|
4783
|
+
output(
|
|
4784
|
+
`${name}: watching since ${res.started_at}
|
|
4785
|
+
Changed: ${res.changed_files} files | Deleted: ${res.deleted_files} files`,
|
|
4786
|
+
false
|
|
4787
|
+
);
|
|
4788
|
+
} else {
|
|
4789
|
+
output(`${name}: not watching. Run \`lens repo watch\` to start.`, false);
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4641
4793
|
// packages/cli/src/index.ts
|
|
4642
|
-
var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.
|
|
4794
|
+
var program2 = new Command().name("lens").description("LENS \u2014 Local-first repo context engine").version("0.1.20");
|
|
4643
4795
|
function trackCommand(name) {
|
|
4644
4796
|
if (!isTelemetryEnabled()) return;
|
|
4645
4797
|
const BASE_URL2 = process.env.LENS_HOST ?? "http://127.0.0.1:4111";
|