@limno/core 0.1.3 → 0.1.4
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/index.cjs +174 -41
- package/dist/index.js +173 -40
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -59,8 +59,8 @@ function jsxName(name) {
|
|
|
59
59
|
return `${jsxName(name.object)}.${name.property.name}`;
|
|
60
60
|
}
|
|
61
61
|
var isCapitalized = (s) => /^[A-Z]/.test(s);
|
|
62
|
-
function componentName(
|
|
63
|
-
let p =
|
|
62
|
+
function componentName(path5, fileBase) {
|
|
63
|
+
let p = path5.getFunctionParent();
|
|
64
64
|
while (p) {
|
|
65
65
|
const node = p.node;
|
|
66
66
|
let name;
|
|
@@ -102,8 +102,8 @@ function stampSource(code, id, relFile) {
|
|
|
102
102
|
const fileBase = relFile.split("/").pop().replace(/\.[jt]sx?$/, "").replace(/^./, (c) => c.toUpperCase());
|
|
103
103
|
let touched = false;
|
|
104
104
|
traverse(ast, {
|
|
105
|
-
JSXOpeningElement(
|
|
106
|
-
const node =
|
|
105
|
+
JSXOpeningElement(path5) {
|
|
106
|
+
const node = path5.node;
|
|
107
107
|
const loc = node.loc;
|
|
108
108
|
if (!loc || node.name.end == null) return;
|
|
109
109
|
const name = jsxName(node.name);
|
|
@@ -111,7 +111,7 @@ function stampSource(code, id, relFile) {
|
|
|
111
111
|
const host = node.name.type === "JSXIdentifier" && !isCapitalized(name);
|
|
112
112
|
if (host) {
|
|
113
113
|
if (hasAttr(node, "data-limno-src")) return;
|
|
114
|
-
const comp = componentName(
|
|
114
|
+
const comp = componentName(path5, fileBase);
|
|
115
115
|
s.appendLeft(node.name.end, ` data-limno-src="${escapeAttr(where)}"${comp ? ` data-limno-comp="${escapeAttr(comp)}"` : ""}`);
|
|
116
116
|
touched = true;
|
|
117
117
|
} else {
|
|
@@ -223,9 +223,9 @@ async function stampTemplate(code, id, relFile, root) {
|
|
|
223
223
|
|
|
224
224
|
// src/service.ts
|
|
225
225
|
var import_node_crypto = require("crypto");
|
|
226
|
-
var
|
|
227
|
-
var
|
|
228
|
-
var
|
|
226
|
+
var import_node_fs3 = require("fs");
|
|
227
|
+
var import_node_module3 = require("module");
|
|
228
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
229
229
|
var import_spec = require("@limno/spec");
|
|
230
230
|
|
|
231
231
|
// src/runner.ts
|
|
@@ -546,22 +546,129 @@ function createRunner(root, folder, onState) {
|
|
|
546
546
|
};
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
+
// src/update.ts
|
|
550
|
+
var import_node_child_process2 = require("child_process");
|
|
551
|
+
var import_node_fs2 = require("fs");
|
|
552
|
+
var import_node_module2 = require("module");
|
|
553
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
554
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
555
|
+
var require2 = (0, import_node_module2.createRequire)(importMetaUrl);
|
|
556
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
557
|
+
function currentVersion() {
|
|
558
|
+
try {
|
|
559
|
+
return require2("../package.json").version;
|
|
560
|
+
} catch {
|
|
561
|
+
return "0.0.0";
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
var newer = (a, b) => {
|
|
565
|
+
const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
566
|
+
const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
567
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
568
|
+
return false;
|
|
569
|
+
};
|
|
570
|
+
function packageManager(root) {
|
|
571
|
+
let dir = root;
|
|
572
|
+
for (; ; ) {
|
|
573
|
+
if ((0, import_node_fs2.existsSync)(import_node_path3.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
574
|
+
if ((0, import_node_fs2.existsSync)(import_node_path3.default.join(dir, "yarn.lock"))) return "yarn";
|
|
575
|
+
if ((0, import_node_fs2.existsSync)(import_node_path3.default.join(dir, "bun.lockb")) || (0, import_node_fs2.existsSync)(import_node_path3.default.join(dir, "bun.lock"))) return "bun";
|
|
576
|
+
if ((0, import_node_fs2.existsSync)(import_node_path3.default.join(dir, "package-lock.json"))) return "npm";
|
|
577
|
+
const up = import_node_path3.default.dirname(dir);
|
|
578
|
+
if (up === dir) return "npm";
|
|
579
|
+
dir = up;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function listedPackages(root) {
|
|
583
|
+
try {
|
|
584
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)(import_node_path3.default.join(root, "package.json"), "utf8"));
|
|
585
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).filter((n) => n.startsWith("@limno/") && n !== "@limno/cli");
|
|
586
|
+
} catch {
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function installArgs(pm, pkgs) {
|
|
591
|
+
const at = pkgs.map((p) => `${p}@latest`);
|
|
592
|
+
if (pm === "pnpm") return ["pnpm", ["add", "-D", ...at]];
|
|
593
|
+
if (pm === "yarn") return ["yarn", ["add", "-D", ...at]];
|
|
594
|
+
if (pm === "bun") return ["bun", ["add", "-d", ...at]];
|
|
595
|
+
return ["npm", ["install", "-D", ...at]];
|
|
596
|
+
}
|
|
597
|
+
var cacheFile = () => import_node_path3.default.join(import_node_os2.default.homedir(), ".limno", "update-check.json");
|
|
598
|
+
async function latestVersion() {
|
|
599
|
+
try {
|
|
600
|
+
const cached = JSON.parse((0, import_node_fs2.readFileSync)(cacheFile(), "utf8"));
|
|
601
|
+
if (Date.now() - cached.at < DAY) return cached.latest;
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const res = await fetch("https://registry.npmjs.org/-/package/@limno/core/dist-tags", { signal: AbortSignal.timeout(4e3) });
|
|
606
|
+
if (!res.ok) return null;
|
|
607
|
+
const latest = (await res.json()).latest ?? null;
|
|
608
|
+
if (latest) {
|
|
609
|
+
(0, import_node_fs2.mkdirSync)(import_node_path3.default.dirname(cacheFile()), { recursive: true });
|
|
610
|
+
(0, import_node_fs2.writeFileSync)(cacheFile(), JSON.stringify({ at: Date.now(), latest }));
|
|
611
|
+
}
|
|
612
|
+
return latest;
|
|
613
|
+
} catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function createUpdater(root) {
|
|
618
|
+
let installing = null;
|
|
619
|
+
async function info() {
|
|
620
|
+
const current = currentVersion();
|
|
621
|
+
if (process.env.LIMNO_NO_UPDATE_CHECK) return { current, latest: null, available: false, command: null };
|
|
622
|
+
const latest = await latestVersion();
|
|
623
|
+
const pkgs = listedPackages(root);
|
|
624
|
+
const [cmd, args] = installArgs(packageManager(root), pkgs);
|
|
625
|
+
const available = !!latest && newer(latest, current) && pkgs.length > 0;
|
|
626
|
+
return { current, latest, available, command: available ? `${cmd} ${args.join(" ")}` : null };
|
|
627
|
+
}
|
|
628
|
+
function install() {
|
|
629
|
+
if (installing) return installing;
|
|
630
|
+
const pkgs = listedPackages(root);
|
|
631
|
+
if (!pkgs.length) return Promise.resolve({ ok: false, output: "No @limno packages found in package.json." });
|
|
632
|
+
const [cmd, args] = installArgs(packageManager(root), pkgs);
|
|
633
|
+
installing = new Promise((resolve) => {
|
|
634
|
+
let output = "";
|
|
635
|
+
const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: root, env: process.env, shell: process.platform === "win32" });
|
|
636
|
+
child.stdout?.on("data", (d) => output += d);
|
|
637
|
+
child.stderr?.on("data", (d) => output += d);
|
|
638
|
+
child.on("error", (e) => resolve({ ok: false, output: String(e) }));
|
|
639
|
+
child.on("close", (code) => {
|
|
640
|
+
if (code === 0) {
|
|
641
|
+
try {
|
|
642
|
+
(0, import_node_fs2.writeFileSync)(cacheFile(), JSON.stringify({ at: 0, latest: "" }));
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
resolve({ ok: code === 0, output: output.trim().split("\n").slice(-12).join("\n") });
|
|
647
|
+
});
|
|
648
|
+
}).finally(() => {
|
|
649
|
+
installing = null;
|
|
650
|
+
});
|
|
651
|
+
return installing;
|
|
652
|
+
}
|
|
653
|
+
return { info, install };
|
|
654
|
+
}
|
|
655
|
+
|
|
549
656
|
// src/service.ts
|
|
550
657
|
var ROUTE = "/__limno";
|
|
551
|
-
var
|
|
658
|
+
var require3 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
552
659
|
var STALE_MS = 3e4;
|
|
553
660
|
function readJson(file) {
|
|
554
661
|
try {
|
|
555
|
-
return JSON.parse((0,
|
|
662
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
|
|
556
663
|
} catch {
|
|
557
664
|
return null;
|
|
558
665
|
}
|
|
559
666
|
}
|
|
560
667
|
function writeAtomic(file, data) {
|
|
561
|
-
(0,
|
|
668
|
+
(0, import_node_fs3.mkdirSync)(import_node_path4.default.dirname(file), { recursive: true });
|
|
562
669
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
563
|
-
(0,
|
|
564
|
-
(0,
|
|
670
|
+
(0, import_node_fs3.writeFileSync)(tmp, data);
|
|
671
|
+
(0, import_node_fs3.renameSync)(tmp, file);
|
|
565
672
|
}
|
|
566
673
|
function body(req) {
|
|
567
674
|
return new Promise((resolve, reject) => {
|
|
@@ -579,12 +686,12 @@ function send(res, status, payload, type = "application/json") {
|
|
|
579
686
|
}
|
|
580
687
|
var hash = (s) => (0, import_node_crypto.createHash)("sha1").update(s).digest("hex");
|
|
581
688
|
function overlayFile() {
|
|
582
|
-
return
|
|
689
|
+
return require3.resolve("@limno/overlay/dist/overlay.js");
|
|
583
690
|
}
|
|
584
691
|
function createLimnoService({ root, dir = ".limno" }) {
|
|
585
|
-
const folder =
|
|
586
|
-
const file =
|
|
587
|
-
const presenceFile =
|
|
692
|
+
const folder = import_node_path4.default.join(root, dir);
|
|
693
|
+
const file = import_node_path4.default.join(folder, "changeset.json");
|
|
694
|
+
const presenceFile = import_node_path4.default.join(folder, "agent.json");
|
|
588
695
|
let lastWrittenHash = "";
|
|
589
696
|
const clients = /* @__PURE__ */ new Set();
|
|
590
697
|
function broadcast(event, data) {
|
|
@@ -595,6 +702,7 @@ data: ${JSON.stringify(data)}
|
|
|
595
702
|
for (const c of clients) c.write(msg);
|
|
596
703
|
}
|
|
597
704
|
const runner = createRunner(root, folder, (state) => broadcast("runner", state));
|
|
705
|
+
const updater = createUpdater(root);
|
|
598
706
|
function setupInfo() {
|
|
599
707
|
return { command: "npx", args: ["-y", "@limno/mcp", "--root", root], root };
|
|
600
708
|
}
|
|
@@ -612,23 +720,23 @@ data: ${JSON.stringify(data)}
|
|
|
612
720
|
return { connected, client: connected ? presence?.client ?? null : null, lastSeen: presence?.lastSeen ?? null, setup: setupInfo() };
|
|
613
721
|
}
|
|
614
722
|
function installConfig(target) {
|
|
615
|
-
const dest = target === "claude" ?
|
|
723
|
+
const dest = target === "claude" ? import_node_path4.default.join(root, ".mcp.json") : import_node_path4.default.join(root, ".cursor", "mcp.json");
|
|
616
724
|
let config = {};
|
|
617
|
-
if ((0,
|
|
725
|
+
if ((0, import_node_fs3.existsSync)(dest)) {
|
|
618
726
|
try {
|
|
619
|
-
config = JSON.parse((0,
|
|
727
|
+
config = JSON.parse((0, import_node_fs3.readFileSync)(dest, "utf8"));
|
|
620
728
|
} catch {
|
|
621
|
-
throw new Error(`${
|
|
729
|
+
throw new Error(`${import_node_path4.default.relative(root, dest)} isn't valid JSON; fix or remove it first.`);
|
|
622
730
|
}
|
|
623
731
|
}
|
|
624
732
|
const { command, args } = setupInfo();
|
|
625
733
|
const portable = args.map((a, i) => {
|
|
626
734
|
if (args[i - 1] === "--root") return ".";
|
|
627
|
-
return
|
|
735
|
+
return import_node_path4.default.isAbsolute(a) ? import_node_path4.default.relative(root, a).split(import_node_path4.default.sep).join("/") : a;
|
|
628
736
|
});
|
|
629
737
|
config.mcpServers = { ...config.mcpServers ?? {}, limno: { command, args: portable } };
|
|
630
738
|
writeAtomic(dest, JSON.stringify(config, null, 2) + "\n");
|
|
631
|
-
return
|
|
739
|
+
return import_node_path4.default.relative(root, dest);
|
|
632
740
|
}
|
|
633
741
|
let lastAgent = "";
|
|
634
742
|
function pushAgent() {
|
|
@@ -639,27 +747,45 @@ data: ${JSON.stringify(data)}
|
|
|
639
747
|
broadcast("agent", status);
|
|
640
748
|
}
|
|
641
749
|
function onChangeSetFile() {
|
|
642
|
-
const raw = (0,
|
|
750
|
+
const raw = (0, import_node_fs3.existsSync)(file) ? (0, import_node_fs3.readFileSync)(file, "utf8") : "";
|
|
643
751
|
if (!raw || hash(raw) === lastWrittenHash) return;
|
|
644
752
|
const set = readJson(file);
|
|
645
753
|
if (set) broadcast("changeset", set);
|
|
646
754
|
}
|
|
647
|
-
(0,
|
|
755
|
+
(0, import_node_fs3.mkdirSync)(folder, { recursive: true });
|
|
648
756
|
let watcher = null;
|
|
757
|
+
let watchedIno = -1;
|
|
649
758
|
let debounce;
|
|
650
|
-
|
|
651
|
-
watcher
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (name
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
759
|
+
function startWatching() {
|
|
760
|
+
watcher?.close();
|
|
761
|
+
watcher = null;
|
|
762
|
+
try {
|
|
763
|
+
watchedIno = (0, import_node_fs3.statSync)(folder).ino;
|
|
764
|
+
watcher = (0, import_node_fs3.watch)(folder, (_event, name) => {
|
|
765
|
+
if (!name || name.endsWith(".tmp")) return;
|
|
766
|
+
clearTimeout(debounce);
|
|
767
|
+
debounce = setTimeout(() => {
|
|
768
|
+
if (name === "changeset.json") onChangeSetFile();
|
|
769
|
+
if (name === "agent.json") pushAgent();
|
|
770
|
+
}, 40);
|
|
771
|
+
});
|
|
772
|
+
watcher.unref?.();
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
661
775
|
}
|
|
662
|
-
|
|
776
|
+
startWatching();
|
|
777
|
+
const presenceTimer = setInterval(() => {
|
|
778
|
+
let ino = -1;
|
|
779
|
+
try {
|
|
780
|
+
ino = (0, import_node_fs3.statSync)(folder).ino;
|
|
781
|
+
} catch {
|
|
782
|
+
}
|
|
783
|
+
if (ino !== -1 && ino !== watchedIno) {
|
|
784
|
+
startWatching();
|
|
785
|
+
onChangeSetFile();
|
|
786
|
+
}
|
|
787
|
+
pushAgent();
|
|
788
|
+
}, 1e4);
|
|
663
789
|
presenceTimer.unref();
|
|
664
790
|
const keepAlive = setInterval(() => {
|
|
665
791
|
for (const c of clients) c.write(": ping\n\n");
|
|
@@ -668,8 +794,8 @@ data: ${JSON.stringify(data)}
|
|
|
668
794
|
async function openInEditor(spec) {
|
|
669
795
|
const m = /^(.*?)(?::(\d+))?(?::(\d+))?$/.exec(spec);
|
|
670
796
|
if (!m) return false;
|
|
671
|
-
const target =
|
|
672
|
-
if (!target.startsWith(root +
|
|
797
|
+
const target = import_node_path4.default.resolve(root, m[1]);
|
|
798
|
+
if (!target.startsWith(root + import_node_path4.default.sep) || !(0, import_node_fs3.existsSync)(target)) return false;
|
|
673
799
|
const { default: launch } = await import("launch-editor");
|
|
674
800
|
launch(`${target}${m[2] ? `:${m[2]}` : ""}${m[3] ? `:${m[3]}` : ""}`);
|
|
675
801
|
return true;
|
|
@@ -679,7 +805,7 @@ data: ${JSON.stringify(data)}
|
|
|
679
805
|
if (!url.startsWith(ROUTE)) return next ? next() : send(res, 404, { error: "not found" });
|
|
680
806
|
try {
|
|
681
807
|
if (url === `${ROUTE}/overlay.js`) {
|
|
682
|
-
return send(res, 200, (0,
|
|
808
|
+
return send(res, 200, (0, import_node_fs3.readFileSync)(overlayFile(), "utf8"), "text/javascript");
|
|
683
809
|
}
|
|
684
810
|
if (url === `${ROUTE}/events`) {
|
|
685
811
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" });
|
|
@@ -716,6 +842,13 @@ data: ${JSON.stringify(data)}
|
|
|
716
842
|
return send(res, 409, { error: e instanceof Error ? e.message : String(e) });
|
|
717
843
|
}
|
|
718
844
|
}
|
|
845
|
+
if (url === `${ROUTE}/update` && req.method === "GET") {
|
|
846
|
+
return send(res, 200, await updater.info());
|
|
847
|
+
}
|
|
848
|
+
if (url === `${ROUTE}/update` && req.method === "POST") {
|
|
849
|
+
if (!isLocalRequest(req)) return send(res, 403, { error: "Only available from this computer" });
|
|
850
|
+
return send(res, 200, await updater.install());
|
|
851
|
+
}
|
|
719
852
|
if (url === `${ROUTE}/runner` && req.method === "GET") {
|
|
720
853
|
return send(res, 200, await runner.info(new URLSearchParams(query).has("refresh")));
|
|
721
854
|
}
|
|
@@ -743,7 +876,7 @@ data: ${JSON.stringify(data)}
|
|
|
743
876
|
return send(res, await openInEditor(spec) ? 200 : 404, { ok: true });
|
|
744
877
|
}
|
|
745
878
|
if (url === `${ROUTE}/info`) {
|
|
746
|
-
return send(res, 200, { name: "limno", root, file:
|
|
879
|
+
return send(res, 200, { name: "limno", root, file: import_node_path4.default.relative(root, file) });
|
|
747
880
|
}
|
|
748
881
|
send(res, 404, { error: "not found" });
|
|
749
882
|
} catch (err) {
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@ function jsxName(name) {
|
|
|
10
10
|
return `${jsxName(name.object)}.${name.property.name}`;
|
|
11
11
|
}
|
|
12
12
|
var isCapitalized = (s) => /^[A-Z]/.test(s);
|
|
13
|
-
function componentName(
|
|
14
|
-
let p =
|
|
13
|
+
function componentName(path5, fileBase) {
|
|
14
|
+
let p = path5.getFunctionParent();
|
|
15
15
|
while (p) {
|
|
16
16
|
const node = p.node;
|
|
17
17
|
let name;
|
|
@@ -53,8 +53,8 @@ function stampSource(code, id, relFile) {
|
|
|
53
53
|
const fileBase = relFile.split("/").pop().replace(/\.[jt]sx?$/, "").replace(/^./, (c) => c.toUpperCase());
|
|
54
54
|
let touched = false;
|
|
55
55
|
traverse(ast, {
|
|
56
|
-
JSXOpeningElement(
|
|
57
|
-
const node =
|
|
56
|
+
JSXOpeningElement(path5) {
|
|
57
|
+
const node = path5.node;
|
|
58
58
|
const loc = node.loc;
|
|
59
59
|
if (!loc || node.name.end == null) return;
|
|
60
60
|
const name = jsxName(node.name);
|
|
@@ -62,7 +62,7 @@ function stampSource(code, id, relFile) {
|
|
|
62
62
|
const host = node.name.type === "JSXIdentifier" && !isCapitalized(name);
|
|
63
63
|
if (host) {
|
|
64
64
|
if (hasAttr(node, "data-limno-src")) return;
|
|
65
|
-
const comp = componentName(
|
|
65
|
+
const comp = componentName(path5, fileBase);
|
|
66
66
|
s.appendLeft(node.name.end, ` data-limno-src="${escapeAttr(where)}"${comp ? ` data-limno-comp="${escapeAttr(comp)}"` : ""}`);
|
|
67
67
|
touched = true;
|
|
68
68
|
} else {
|
|
@@ -174,9 +174,9 @@ async function stampTemplate(code, id, relFile, root) {
|
|
|
174
174
|
|
|
175
175
|
// src/service.ts
|
|
176
176
|
import { createHash } from "crypto";
|
|
177
|
-
import { existsSync, mkdirSync as
|
|
178
|
-
import { createRequire as
|
|
179
|
-
import
|
|
177
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, statSync, watch, writeFileSync as writeFileSync3 } from "fs";
|
|
178
|
+
import { createRequire as createRequire3 } from "module";
|
|
179
|
+
import path4 from "path";
|
|
180
180
|
import { emptyChangeSet, mergeStatuses, renderPrompt } from "@limno/spec";
|
|
181
181
|
|
|
182
182
|
// src/runner.ts
|
|
@@ -497,21 +497,128 @@ function createRunner(root, folder, onState) {
|
|
|
497
497
|
};
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// src/update.ts
|
|
501
|
+
import { spawn as spawn2 } from "child_process";
|
|
502
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
503
|
+
import { createRequire as createRequire2 } from "module";
|
|
504
|
+
import os2 from "os";
|
|
505
|
+
import path3 from "path";
|
|
506
|
+
var require2 = createRequire2(import.meta.url);
|
|
507
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
508
|
+
function currentVersion() {
|
|
509
|
+
try {
|
|
510
|
+
return require2("../package.json").version;
|
|
511
|
+
} catch {
|
|
512
|
+
return "0.0.0";
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
var newer = (a, b) => {
|
|
516
|
+
const pa = a.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
517
|
+
const pb = b.split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
518
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
519
|
+
return false;
|
|
520
|
+
};
|
|
521
|
+
function packageManager(root) {
|
|
522
|
+
let dir = root;
|
|
523
|
+
for (; ; ) {
|
|
524
|
+
if (existsSync(path3.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
525
|
+
if (existsSync(path3.join(dir, "yarn.lock"))) return "yarn";
|
|
526
|
+
if (existsSync(path3.join(dir, "bun.lockb")) || existsSync(path3.join(dir, "bun.lock"))) return "bun";
|
|
527
|
+
if (existsSync(path3.join(dir, "package-lock.json"))) return "npm";
|
|
528
|
+
const up = path3.dirname(dir);
|
|
529
|
+
if (up === dir) return "npm";
|
|
530
|
+
dir = up;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function listedPackages(root) {
|
|
534
|
+
try {
|
|
535
|
+
const pkg = JSON.parse(readFileSync2(path3.join(root, "package.json"), "utf8"));
|
|
536
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).filter((n) => n.startsWith("@limno/") && n !== "@limno/cli");
|
|
537
|
+
} catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function installArgs(pm, pkgs) {
|
|
542
|
+
const at = pkgs.map((p) => `${p}@latest`);
|
|
543
|
+
if (pm === "pnpm") return ["pnpm", ["add", "-D", ...at]];
|
|
544
|
+
if (pm === "yarn") return ["yarn", ["add", "-D", ...at]];
|
|
545
|
+
if (pm === "bun") return ["bun", ["add", "-d", ...at]];
|
|
546
|
+
return ["npm", ["install", "-D", ...at]];
|
|
547
|
+
}
|
|
548
|
+
var cacheFile = () => path3.join(os2.homedir(), ".limno", "update-check.json");
|
|
549
|
+
async function latestVersion() {
|
|
550
|
+
try {
|
|
551
|
+
const cached = JSON.parse(readFileSync2(cacheFile(), "utf8"));
|
|
552
|
+
if (Date.now() - cached.at < DAY) return cached.latest;
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const res = await fetch("https://registry.npmjs.org/-/package/@limno/core/dist-tags", { signal: AbortSignal.timeout(4e3) });
|
|
557
|
+
if (!res.ok) return null;
|
|
558
|
+
const latest = (await res.json()).latest ?? null;
|
|
559
|
+
if (latest) {
|
|
560
|
+
mkdirSync2(path3.dirname(cacheFile()), { recursive: true });
|
|
561
|
+
writeFileSync2(cacheFile(), JSON.stringify({ at: Date.now(), latest }));
|
|
562
|
+
}
|
|
563
|
+
return latest;
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function createUpdater(root) {
|
|
569
|
+
let installing = null;
|
|
570
|
+
async function info() {
|
|
571
|
+
const current = currentVersion();
|
|
572
|
+
if (process.env.LIMNO_NO_UPDATE_CHECK) return { current, latest: null, available: false, command: null };
|
|
573
|
+
const latest = await latestVersion();
|
|
574
|
+
const pkgs = listedPackages(root);
|
|
575
|
+
const [cmd, args] = installArgs(packageManager(root), pkgs);
|
|
576
|
+
const available = !!latest && newer(latest, current) && pkgs.length > 0;
|
|
577
|
+
return { current, latest, available, command: available ? `${cmd} ${args.join(" ")}` : null };
|
|
578
|
+
}
|
|
579
|
+
function install() {
|
|
580
|
+
if (installing) return installing;
|
|
581
|
+
const pkgs = listedPackages(root);
|
|
582
|
+
if (!pkgs.length) return Promise.resolve({ ok: false, output: "No @limno packages found in package.json." });
|
|
583
|
+
const [cmd, args] = installArgs(packageManager(root), pkgs);
|
|
584
|
+
installing = new Promise((resolve) => {
|
|
585
|
+
let output = "";
|
|
586
|
+
const child = spawn2(cmd, args, { cwd: root, env: process.env, shell: process.platform === "win32" });
|
|
587
|
+
child.stdout?.on("data", (d) => output += d);
|
|
588
|
+
child.stderr?.on("data", (d) => output += d);
|
|
589
|
+
child.on("error", (e) => resolve({ ok: false, output: String(e) }));
|
|
590
|
+
child.on("close", (code) => {
|
|
591
|
+
if (code === 0) {
|
|
592
|
+
try {
|
|
593
|
+
writeFileSync2(cacheFile(), JSON.stringify({ at: 0, latest: "" }));
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
resolve({ ok: code === 0, output: output.trim().split("\n").slice(-12).join("\n") });
|
|
598
|
+
});
|
|
599
|
+
}).finally(() => {
|
|
600
|
+
installing = null;
|
|
601
|
+
});
|
|
602
|
+
return installing;
|
|
603
|
+
}
|
|
604
|
+
return { info, install };
|
|
605
|
+
}
|
|
606
|
+
|
|
500
607
|
// src/service.ts
|
|
501
608
|
var ROUTE = "/__limno";
|
|
502
|
-
var
|
|
609
|
+
var require3 = createRequire3(import.meta.url);
|
|
503
610
|
var STALE_MS = 3e4;
|
|
504
611
|
function readJson(file) {
|
|
505
612
|
try {
|
|
506
|
-
return JSON.parse(
|
|
613
|
+
return JSON.parse(readFileSync3(file, "utf8"));
|
|
507
614
|
} catch {
|
|
508
615
|
return null;
|
|
509
616
|
}
|
|
510
617
|
}
|
|
511
618
|
function writeAtomic(file, data) {
|
|
512
|
-
|
|
619
|
+
mkdirSync3(path4.dirname(file), { recursive: true });
|
|
513
620
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
514
|
-
|
|
621
|
+
writeFileSync3(tmp, data);
|
|
515
622
|
renameSync(tmp, file);
|
|
516
623
|
}
|
|
517
624
|
function body(req) {
|
|
@@ -530,12 +637,12 @@ function send(res, status, payload, type = "application/json") {
|
|
|
530
637
|
}
|
|
531
638
|
var hash = (s) => createHash("sha1").update(s).digest("hex");
|
|
532
639
|
function overlayFile() {
|
|
533
|
-
return
|
|
640
|
+
return require3.resolve("@limno/overlay/dist/overlay.js");
|
|
534
641
|
}
|
|
535
642
|
function createLimnoService({ root, dir = ".limno" }) {
|
|
536
|
-
const folder =
|
|
537
|
-
const file =
|
|
538
|
-
const presenceFile =
|
|
643
|
+
const folder = path4.join(root, dir);
|
|
644
|
+
const file = path4.join(folder, "changeset.json");
|
|
645
|
+
const presenceFile = path4.join(folder, "agent.json");
|
|
539
646
|
let lastWrittenHash = "";
|
|
540
647
|
const clients = /* @__PURE__ */ new Set();
|
|
541
648
|
function broadcast(event, data) {
|
|
@@ -546,6 +653,7 @@ data: ${JSON.stringify(data)}
|
|
|
546
653
|
for (const c of clients) c.write(msg);
|
|
547
654
|
}
|
|
548
655
|
const runner = createRunner(root, folder, (state) => broadcast("runner", state));
|
|
656
|
+
const updater = createUpdater(root);
|
|
549
657
|
function setupInfo() {
|
|
550
658
|
return { command: "npx", args: ["-y", "@limno/mcp", "--root", root], root };
|
|
551
659
|
}
|
|
@@ -563,23 +671,23 @@ data: ${JSON.stringify(data)}
|
|
|
563
671
|
return { connected, client: connected ? presence?.client ?? null : null, lastSeen: presence?.lastSeen ?? null, setup: setupInfo() };
|
|
564
672
|
}
|
|
565
673
|
function installConfig(target) {
|
|
566
|
-
const dest = target === "claude" ?
|
|
674
|
+
const dest = target === "claude" ? path4.join(root, ".mcp.json") : path4.join(root, ".cursor", "mcp.json");
|
|
567
675
|
let config = {};
|
|
568
|
-
if (
|
|
676
|
+
if (existsSync2(dest)) {
|
|
569
677
|
try {
|
|
570
|
-
config = JSON.parse(
|
|
678
|
+
config = JSON.parse(readFileSync3(dest, "utf8"));
|
|
571
679
|
} catch {
|
|
572
|
-
throw new Error(`${
|
|
680
|
+
throw new Error(`${path4.relative(root, dest)} isn't valid JSON; fix or remove it first.`);
|
|
573
681
|
}
|
|
574
682
|
}
|
|
575
683
|
const { command, args } = setupInfo();
|
|
576
684
|
const portable = args.map((a, i) => {
|
|
577
685
|
if (args[i - 1] === "--root") return ".";
|
|
578
|
-
return
|
|
686
|
+
return path4.isAbsolute(a) ? path4.relative(root, a).split(path4.sep).join("/") : a;
|
|
579
687
|
});
|
|
580
688
|
config.mcpServers = { ...config.mcpServers ?? {}, limno: { command, args: portable } };
|
|
581
689
|
writeAtomic(dest, JSON.stringify(config, null, 2) + "\n");
|
|
582
|
-
return
|
|
690
|
+
return path4.relative(root, dest);
|
|
583
691
|
}
|
|
584
692
|
let lastAgent = "";
|
|
585
693
|
function pushAgent() {
|
|
@@ -590,27 +698,45 @@ data: ${JSON.stringify(data)}
|
|
|
590
698
|
broadcast("agent", status);
|
|
591
699
|
}
|
|
592
700
|
function onChangeSetFile() {
|
|
593
|
-
const raw =
|
|
701
|
+
const raw = existsSync2(file) ? readFileSync3(file, "utf8") : "";
|
|
594
702
|
if (!raw || hash(raw) === lastWrittenHash) return;
|
|
595
703
|
const set = readJson(file);
|
|
596
704
|
if (set) broadcast("changeset", set);
|
|
597
705
|
}
|
|
598
|
-
|
|
706
|
+
mkdirSync3(folder, { recursive: true });
|
|
599
707
|
let watcher = null;
|
|
708
|
+
let watchedIno = -1;
|
|
600
709
|
let debounce;
|
|
601
|
-
|
|
602
|
-
watcher
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (name
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
710
|
+
function startWatching() {
|
|
711
|
+
watcher?.close();
|
|
712
|
+
watcher = null;
|
|
713
|
+
try {
|
|
714
|
+
watchedIno = statSync(folder).ino;
|
|
715
|
+
watcher = watch(folder, (_event, name) => {
|
|
716
|
+
if (!name || name.endsWith(".tmp")) return;
|
|
717
|
+
clearTimeout(debounce);
|
|
718
|
+
debounce = setTimeout(() => {
|
|
719
|
+
if (name === "changeset.json") onChangeSetFile();
|
|
720
|
+
if (name === "agent.json") pushAgent();
|
|
721
|
+
}, 40);
|
|
722
|
+
});
|
|
723
|
+
watcher.unref?.();
|
|
724
|
+
} catch {
|
|
725
|
+
}
|
|
612
726
|
}
|
|
613
|
-
|
|
727
|
+
startWatching();
|
|
728
|
+
const presenceTimer = setInterval(() => {
|
|
729
|
+
let ino = -1;
|
|
730
|
+
try {
|
|
731
|
+
ino = statSync(folder).ino;
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
if (ino !== -1 && ino !== watchedIno) {
|
|
735
|
+
startWatching();
|
|
736
|
+
onChangeSetFile();
|
|
737
|
+
}
|
|
738
|
+
pushAgent();
|
|
739
|
+
}, 1e4);
|
|
614
740
|
presenceTimer.unref();
|
|
615
741
|
const keepAlive = setInterval(() => {
|
|
616
742
|
for (const c of clients) c.write(": ping\n\n");
|
|
@@ -619,8 +745,8 @@ data: ${JSON.stringify(data)}
|
|
|
619
745
|
async function openInEditor(spec) {
|
|
620
746
|
const m = /^(.*?)(?::(\d+))?(?::(\d+))?$/.exec(spec);
|
|
621
747
|
if (!m) return false;
|
|
622
|
-
const target =
|
|
623
|
-
if (!target.startsWith(root +
|
|
748
|
+
const target = path4.resolve(root, m[1]);
|
|
749
|
+
if (!target.startsWith(root + path4.sep) || !existsSync2(target)) return false;
|
|
624
750
|
const { default: launch } = await import("launch-editor");
|
|
625
751
|
launch(`${target}${m[2] ? `:${m[2]}` : ""}${m[3] ? `:${m[3]}` : ""}`);
|
|
626
752
|
return true;
|
|
@@ -630,7 +756,7 @@ data: ${JSON.stringify(data)}
|
|
|
630
756
|
if (!url.startsWith(ROUTE)) return next ? next() : send(res, 404, { error: "not found" });
|
|
631
757
|
try {
|
|
632
758
|
if (url === `${ROUTE}/overlay.js`) {
|
|
633
|
-
return send(res, 200,
|
|
759
|
+
return send(res, 200, readFileSync3(overlayFile(), "utf8"), "text/javascript");
|
|
634
760
|
}
|
|
635
761
|
if (url === `${ROUTE}/events`) {
|
|
636
762
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" });
|
|
@@ -667,6 +793,13 @@ data: ${JSON.stringify(data)}
|
|
|
667
793
|
return send(res, 409, { error: e instanceof Error ? e.message : String(e) });
|
|
668
794
|
}
|
|
669
795
|
}
|
|
796
|
+
if (url === `${ROUTE}/update` && req.method === "GET") {
|
|
797
|
+
return send(res, 200, await updater.info());
|
|
798
|
+
}
|
|
799
|
+
if (url === `${ROUTE}/update` && req.method === "POST") {
|
|
800
|
+
if (!isLocalRequest(req)) return send(res, 403, { error: "Only available from this computer" });
|
|
801
|
+
return send(res, 200, await updater.install());
|
|
802
|
+
}
|
|
670
803
|
if (url === `${ROUTE}/runner` && req.method === "GET") {
|
|
671
804
|
return send(res, 200, await runner.info(new URLSearchParams(query).has("refresh")));
|
|
672
805
|
}
|
|
@@ -694,7 +827,7 @@ data: ${JSON.stringify(data)}
|
|
|
694
827
|
return send(res, await openInEditor(spec) ? 200 : 404, { ok: true });
|
|
695
828
|
}
|
|
696
829
|
if (url === `${ROUTE}/info`) {
|
|
697
|
-
return send(res, 200, { name: "limno", root, file:
|
|
830
|
+
return send(res, 200, { name: "limno", root, file: path4.relative(root, file) });
|
|
698
831
|
}
|
|
699
832
|
send(res, 404, { error: "not found" });
|
|
700
833
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limno/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Limno's framework-neutral dev tooling: JSX source stamping, the change-set store and the endpoints the editor talks to.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@babel/parser": "^7.29.0",
|
|
44
44
|
"@babel/traverse": "^7.29.0",
|
|
45
|
-
"@limno/mcp": "0.1.
|
|
46
|
-
"@limno/overlay": "0.1.
|
|
47
|
-
"@limno/spec": "0.1.
|
|
45
|
+
"@limno/mcp": "0.1.2",
|
|
46
|
+
"@limno/overlay": "0.1.4",
|
|
47
|
+
"@limno/spec": "0.1.2",
|
|
48
48
|
"launch-editor": "^2.14.1",
|
|
49
49
|
"magic-string": "^0.30.17"
|
|
50
50
|
},
|