@merittdev/horus 0.8.0 → 0.8.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/index.cjs +123 -20
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65880,8 +65880,8 @@ init_cjs_shims();
|
|
|
65880
65880
|
|
|
65881
65881
|
// ../../packages/core/src/version.ts
|
|
65882
65882
|
init_cjs_shims();
|
|
65883
|
-
var HORUS_VERSION = true ? "0.8.
|
|
65884
|
-
var PINNED_SOURCE_VERSION = "1.0
|
|
65883
|
+
var HORUS_VERSION = true ? "0.8.1" : "dev";
|
|
65884
|
+
var PINNED_SOURCE_VERSION = "1.4.0";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
65887
65887
|
init_cjs_shims();
|
|
@@ -70979,6 +70979,24 @@ async function getSourceVersion() {
|
|
|
70979
70979
|
return null;
|
|
70980
70980
|
}
|
|
70981
70981
|
}
|
|
70982
|
+
var SourceVersionMismatchError = class extends Error {
|
|
70983
|
+
constructor(installed, pinned) {
|
|
70984
|
+
super(
|
|
70985
|
+
`horus-source ${installed} is installed but Horus is pinned to ${pinned}. A drifted backend builds a graph this CLI cannot map and can corrupt the index (e.g. duplicate-primary-key failures during "Running initial index"). Install the pinned version: pip install 'horus-source==${pinned}'`
|
|
70986
|
+
);
|
|
70987
|
+
this.installed = installed;
|
|
70988
|
+
this.pinned = pinned;
|
|
70989
|
+
this.name = "SourceVersionMismatchError";
|
|
70990
|
+
}
|
|
70991
|
+
installed;
|
|
70992
|
+
pinned;
|
|
70993
|
+
};
|
|
70994
|
+
async function assertSourceVersionPinned() {
|
|
70995
|
+
const installed = await getSourceVersion();
|
|
70996
|
+
if (installed !== null && installed !== PINNED_SOURCE_VERSION) {
|
|
70997
|
+
throw new SourceVersionMismatchError(installed, PINNED_SOURCE_VERSION);
|
|
70998
|
+
}
|
|
70999
|
+
}
|
|
70982
71000
|
function isAnalyzed(root) {
|
|
70983
71001
|
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source"));
|
|
70984
71002
|
}
|
|
@@ -71001,6 +71019,21 @@ function readSourceHostUrl(root) {
|
|
|
71001
71019
|
return null;
|
|
71002
71020
|
}
|
|
71003
71021
|
}
|
|
71022
|
+
function readSourceHostPid(root) {
|
|
71023
|
+
const p5 = (0, import_node_path3.join)(root, ".horus", "source", "host.json");
|
|
71024
|
+
if (!(0, import_node_fs3.existsSync)(p5)) return null;
|
|
71025
|
+
try {
|
|
71026
|
+
const j3 = JSON.parse((0, import_node_fs3.readFileSync)(p5, "utf8"));
|
|
71027
|
+
if (typeof j3.pid !== "number" || !Number.isInteger(j3.pid) || j3.pid <= 0) return null;
|
|
71028
|
+
return {
|
|
71029
|
+
pid: j3.pid,
|
|
71030
|
+
port: typeof j3.port === "number" ? j3.port : NaN,
|
|
71031
|
+
repoPath: typeof j3.repo_path === "string" ? j3.repo_path : ""
|
|
71032
|
+
};
|
|
71033
|
+
} catch {
|
|
71034
|
+
return null;
|
|
71035
|
+
}
|
|
71036
|
+
}
|
|
71004
71037
|
async function isHostHealthy(hostUrl) {
|
|
71005
71038
|
try {
|
|
71006
71039
|
const res = await fetch(`${hostUrl}/api/health`, {
|
|
@@ -97105,6 +97138,12 @@ async function runIndex(opts) {
|
|
|
97105
97138
|
console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
|
|
97106
97139
|
return 1;
|
|
97107
97140
|
}
|
|
97141
|
+
try {
|
|
97142
|
+
await assertSourceVersionPinned();
|
|
97143
|
+
} catch (err2) {
|
|
97144
|
+
console.error(import_picocolors3.default.red(` ${err2.message}`));
|
|
97145
|
+
return 1;
|
|
97146
|
+
}
|
|
97108
97147
|
if (!isAnalyzed(root)) {
|
|
97109
97148
|
console.log(import_picocolors3.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
|
|
97110
97149
|
try {
|
|
@@ -110066,6 +110105,8 @@ function ensureHostReasonHint(reason) {
|
|
|
110066
110105
|
switch (reason) {
|
|
110067
110106
|
case "source-unavailable":
|
|
110068
110107
|
return "horus-source is not installed \u2014 install it: pip install horus-source";
|
|
110108
|
+
case "version-mismatch":
|
|
110109
|
+
return "the installed horus-source backend does not match the version Horus is pinned to \u2014 reinstall the pinned version (run `horus status` to see it)";
|
|
110069
110110
|
case "not-analyzed":
|
|
110070
110111
|
return "this repo has not been indexed yet \u2014 run: horus index";
|
|
110071
110112
|
case "bad-url":
|
|
@@ -110077,6 +110118,11 @@ function ensureHostReasonHint(reason) {
|
|
|
110077
110118
|
async function ensureSourceHost(root, hostUrl, opts = {}) {
|
|
110078
110119
|
if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
|
|
110079
110120
|
if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
|
|
110121
|
+
try {
|
|
110122
|
+
await assertSourceVersionPinned();
|
|
110123
|
+
} catch {
|
|
110124
|
+
return { ok: false, reason: "version-mismatch" };
|
|
110125
|
+
}
|
|
110080
110126
|
if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
|
|
110081
110127
|
const port = parseHostPort(hostUrl);
|
|
110082
110128
|
if (port === null) return { ok: false, reason: "bad-url" };
|
|
@@ -113530,16 +113576,28 @@ async function runStop(opts) {
|
|
|
113530
113576
|
}
|
|
113531
113577
|
}
|
|
113532
113578
|
async function stopHost(root, hostUrl) {
|
|
113533
|
-
const alive = await isHostHealthy(hostUrl);
|
|
113534
|
-
if (!alive) {
|
|
113535
|
-
console.log(import_picocolors36.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
113536
|
-
return 0;
|
|
113537
|
-
}
|
|
113538
113579
|
const port = extractPort(hostUrl);
|
|
113539
113580
|
if (port === null) {
|
|
113540
113581
|
console.error(import_picocolors36.default.red(`Cannot determine port from host URL: ${hostUrl}`));
|
|
113541
113582
|
return 1;
|
|
113542
113583
|
}
|
|
113584
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113585
|
+
if (!healthy) {
|
|
113586
|
+
const owned = readSpawnedHost(root);
|
|
113587
|
+
const wrapperAlive = owned !== null && await getProcessInfo(owned.pid) !== null;
|
|
113588
|
+
if (!wrapperAlive) {
|
|
113589
|
+
const res = await stopBackendServerPid(root, port);
|
|
113590
|
+
if (res === "failed") return 1;
|
|
113591
|
+
if (res !== "stopped") console.log(import_picocolors36.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
113592
|
+
await cleanupSpawnedRecord(root);
|
|
113593
|
+
return 0;
|
|
113594
|
+
}
|
|
113595
|
+
console.log(
|
|
113596
|
+
import_picocolors36.default.dim(
|
|
113597
|
+
`Host ${hostUrl} is unreachable but pid ${owned.pid} is still running \u2014 terminating a stuck host.`
|
|
113598
|
+
)
|
|
113599
|
+
);
|
|
113600
|
+
}
|
|
113543
113601
|
const spawned = readSpawnedHost(root);
|
|
113544
113602
|
if (spawned === null) {
|
|
113545
113603
|
console.error(
|
|
@@ -113572,21 +113630,18 @@ async function stopHost(root, hostUrl) {
|
|
|
113572
113630
|
}
|
|
113573
113631
|
const info2 = await getProcessInfo(spawned.pid);
|
|
113574
113632
|
if (info2 === null) {
|
|
113575
|
-
|
|
113576
|
-
|
|
113577
|
-
|
|
113578
|
-
|
|
113633
|
+
const res = await stopBackendServerPid(root, port);
|
|
113634
|
+
if (res === "failed") return 1;
|
|
113635
|
+
if (res !== "stopped") {
|
|
113636
|
+
console.log(import_picocolors36.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
|
|
113579
113637
|
}
|
|
113638
|
+
await cleanupSpawnedRecord(root);
|
|
113580
113639
|
return 0;
|
|
113581
113640
|
}
|
|
113582
|
-
|
|
113583
|
-
const hostPortRe = new RegExp(
|
|
113584
|
-
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
|
|
113585
|
-
);
|
|
113586
|
-
if (!hostPortRe.test(info2.args)) {
|
|
113641
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113587
113642
|
console.error(
|
|
113588
113643
|
import_picocolors36.default.red(
|
|
113589
|
-
`Pid ${spawned.pid} args do not match "horus-source host --port ${
|
|
113644
|
+
`Pid ${spawned.pid} args do not match "horus-source host --port ${port}". Got: "${info2.args.slice(0, 120)}". Aborting for safety.`
|
|
113590
113645
|
)
|
|
113591
113646
|
);
|
|
113592
113647
|
return 1;
|
|
@@ -113639,11 +113694,57 @@ async function stopHost(root, hostUrl) {
|
|
|
113639
113694
|
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
113640
113695
|
);
|
|
113641
113696
|
}
|
|
113697
|
+
if (await stopBackendServerPid(root, port) === "failed") return 1;
|
|
113698
|
+
await cleanupSpawnedRecord(root);
|
|
113699
|
+
return 0;
|
|
113700
|
+
}
|
|
113701
|
+
function hostArgvRegex(port) {
|
|
113702
|
+
return new RegExp(
|
|
113703
|
+
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${String(port)}(?=\\s|$)`
|
|
113704
|
+
);
|
|
113705
|
+
}
|
|
113706
|
+
async function cleanupSpawnedRecord(root) {
|
|
113642
113707
|
try {
|
|
113643
113708
|
await unlinkAsync((0, import_node_path18.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
113644
113709
|
} catch {
|
|
113645
113710
|
}
|
|
113646
|
-
|
|
113711
|
+
}
|
|
113712
|
+
async function stopBackendServerPid(root, port) {
|
|
113713
|
+
const rec = readSourceHostPid(root);
|
|
113714
|
+
if (!rec) return "none";
|
|
113715
|
+
if (Number.isFinite(rec.port) && rec.port !== port) return "none";
|
|
113716
|
+
if (rec.repoPath && rec.repoPath !== root) return "none";
|
|
113717
|
+
const info2 = await getProcessInfo(rec.pid);
|
|
113718
|
+
if (info2 === null) return "none";
|
|
113719
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113720
|
+
console.error(
|
|
113721
|
+
import_picocolors36.default.red(
|
|
113722
|
+
`Backend host pid ${rec.pid} args do not match "horus-source host --port ${port}". Not signalling \u2014 possible stale source/host.json or PID reuse.`
|
|
113723
|
+
)
|
|
113724
|
+
);
|
|
113725
|
+
return "failed";
|
|
113726
|
+
}
|
|
113727
|
+
try {
|
|
113728
|
+
process.kill(rec.pid, "SIGTERM");
|
|
113729
|
+
} catch (err2) {
|
|
113730
|
+
if (err2.code === "ESRCH") return "stopped";
|
|
113731
|
+
console.error(import_picocolors36.default.red(`Failed to signal pid ${rec.pid}: ${err2.message}`));
|
|
113732
|
+
return "failed";
|
|
113733
|
+
}
|
|
113734
|
+
const deadline = Date.now() + STOP_WAIT_MS;
|
|
113735
|
+
while (Date.now() < deadline) {
|
|
113736
|
+
await sleep(STOP_POLL_MS);
|
|
113737
|
+
if (await getProcessInfo(rec.pid) === null) {
|
|
113738
|
+
console.log(
|
|
113739
|
+
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${rec.pid}, port ${port})`) + ` for ${root}`
|
|
113740
|
+
);
|
|
113741
|
+
return "stopped";
|
|
113742
|
+
}
|
|
113743
|
+
}
|
|
113744
|
+
console.error(
|
|
113745
|
+
import_picocolors36.default.red(`Host pid ${rec.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`)
|
|
113746
|
+
);
|
|
113747
|
+
return "failed";
|
|
113647
113748
|
}
|
|
113648
113749
|
async function stopAll() {
|
|
113649
113750
|
const registry2 = readRegistry();
|
|
@@ -113657,8 +113758,10 @@ async function stopAll() {
|
|
|
113657
113758
|
for (const [name2, entry2] of projects2) {
|
|
113658
113759
|
const hostUrl = readSourceHostUrl(entry2.root);
|
|
113659
113760
|
if (!hostUrl) continue;
|
|
113660
|
-
const
|
|
113661
|
-
if (!
|
|
113761
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113762
|
+
if (!healthy && readSpawnedHost(entry2.root) === null && readSourceHostPid(entry2.root) === null) {
|
|
113763
|
+
continue;
|
|
113764
|
+
}
|
|
113662
113765
|
console.log(` Stopping ${import_picocolors36.default.bold(name2)} ${import_picocolors36.default.dim(`(${hostUrl})`)}`);
|
|
113663
113766
|
const code = await stopHost(entry2.root, hostUrl);
|
|
113664
113767
|
if (code === 0) stopped++;
|