@merittdev/horus 0.8.0 → 0.8.2
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 +143 -21
- 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.2" : "dev";
|
|
65884
|
+
var PINNED_SOURCE_VERSION = "1.5.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`, {
|
|
@@ -71067,6 +71100,21 @@ function removeSpawnedHostRecord(root) {
|
|
|
71067
71100
|
} catch {
|
|
71068
71101
|
}
|
|
71069
71102
|
}
|
|
71103
|
+
function reconcileSpawnedHostPid(root, port) {
|
|
71104
|
+
try {
|
|
71105
|
+
const backend = readSourceHostPid(root);
|
|
71106
|
+
if (!backend || !Number.isInteger(backend.pid) || backend.pid <= 0) return;
|
|
71107
|
+
if (Number.isFinite(backend.port) && backend.port !== port) return;
|
|
71108
|
+
const spawned = readSpawnedHost(root);
|
|
71109
|
+
if (!spawned || spawned.pid === backend.pid) return;
|
|
71110
|
+
const updated = { ...spawned, pid: backend.pid };
|
|
71111
|
+
(0, import_node_fs3.writeFileSync)(
|
|
71112
|
+
(0, import_node_path3.join)(root, ".horus", SPAWNED_HOST_FILE),
|
|
71113
|
+
JSON.stringify(updated, null, 2) + "\n"
|
|
71114
|
+
);
|
|
71115
|
+
} catch {
|
|
71116
|
+
}
|
|
71117
|
+
}
|
|
71070
71118
|
async function waitForHost(hostUrl, timeoutMs = 45e3) {
|
|
71071
71119
|
const deadline = Date.now() + timeoutMs;
|
|
71072
71120
|
while (Date.now() < deadline) {
|
|
@@ -97105,6 +97153,12 @@ async function runIndex(opts) {
|
|
|
97105
97153
|
console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
|
|
97106
97154
|
return 1;
|
|
97107
97155
|
}
|
|
97156
|
+
try {
|
|
97157
|
+
await assertSourceVersionPinned();
|
|
97158
|
+
} catch (err2) {
|
|
97159
|
+
console.error(import_picocolors3.default.red(` ${err2.message}`));
|
|
97160
|
+
return 1;
|
|
97161
|
+
}
|
|
97108
97162
|
if (!isAnalyzed(root)) {
|
|
97109
97163
|
console.log(import_picocolors3.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
|
|
97110
97164
|
try {
|
|
@@ -97127,6 +97181,7 @@ async function runIndex(opts) {
|
|
|
97127
97181
|
);
|
|
97128
97182
|
return 1;
|
|
97129
97183
|
}
|
|
97184
|
+
reconcileSpawnedHostPid(root, port);
|
|
97130
97185
|
}
|
|
97131
97186
|
await stitchQueueMap(hostUrl, dbUrl, label);
|
|
97132
97187
|
buildKnowledgeIndex(root, config2, label, configuredProject, opts);
|
|
@@ -110066,6 +110121,8 @@ function ensureHostReasonHint(reason) {
|
|
|
110066
110121
|
switch (reason) {
|
|
110067
110122
|
case "source-unavailable":
|
|
110068
110123
|
return "horus-source is not installed \u2014 install it: pip install horus-source";
|
|
110124
|
+
case "version-mismatch":
|
|
110125
|
+
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
110126
|
case "not-analyzed":
|
|
110070
110127
|
return "this repo has not been indexed yet \u2014 run: horus index";
|
|
110071
110128
|
case "bad-url":
|
|
@@ -110077,11 +110134,19 @@ function ensureHostReasonHint(reason) {
|
|
|
110077
110134
|
async function ensureSourceHost(root, hostUrl, opts = {}) {
|
|
110078
110135
|
if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
|
|
110079
110136
|
if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
|
|
110137
|
+
try {
|
|
110138
|
+
await assertSourceVersionPinned();
|
|
110139
|
+
} catch {
|
|
110140
|
+
return { ok: false, reason: "version-mismatch" };
|
|
110141
|
+
}
|
|
110080
110142
|
if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
|
|
110081
110143
|
const port = parseHostPort(hostUrl);
|
|
110082
110144
|
if (port === null) return { ok: false, reason: "bad-url" };
|
|
110083
110145
|
startHost(root, port);
|
|
110084
|
-
if (await waitForHost(hostUrl, opts.timeoutMs ?? 2e4))
|
|
110146
|
+
if (await waitForHost(hostUrl, opts.timeoutMs ?? 2e4)) {
|
|
110147
|
+
reconcileSpawnedHostPid(root, port);
|
|
110148
|
+
return { ok: true, hostUrl };
|
|
110149
|
+
}
|
|
110085
110150
|
removeSpawnedHostRecord(root);
|
|
110086
110151
|
return { ok: false, reason: "unhealthy" };
|
|
110087
110152
|
}
|
|
@@ -113530,16 +113595,28 @@ async function runStop(opts) {
|
|
|
113530
113595
|
}
|
|
113531
113596
|
}
|
|
113532
113597
|
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
113598
|
const port = extractPort(hostUrl);
|
|
113539
113599
|
if (port === null) {
|
|
113540
113600
|
console.error(import_picocolors36.default.red(`Cannot determine port from host URL: ${hostUrl}`));
|
|
113541
113601
|
return 1;
|
|
113542
113602
|
}
|
|
113603
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113604
|
+
if (!healthy) {
|
|
113605
|
+
const owned = readSpawnedHost(root);
|
|
113606
|
+
const wrapperAlive = owned !== null && await getProcessInfo(owned.pid) !== null;
|
|
113607
|
+
if (!wrapperAlive) {
|
|
113608
|
+
const res = await stopBackendServerPid(root, port);
|
|
113609
|
+
if (res === "failed") return 1;
|
|
113610
|
+
if (res !== "stopped") console.log(import_picocolors36.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
113611
|
+
await cleanupSpawnedRecord(root);
|
|
113612
|
+
return 0;
|
|
113613
|
+
}
|
|
113614
|
+
console.log(
|
|
113615
|
+
import_picocolors36.default.dim(
|
|
113616
|
+
`Host ${hostUrl} is unreachable but pid ${owned.pid} is still running \u2014 terminating a stuck host.`
|
|
113617
|
+
)
|
|
113618
|
+
);
|
|
113619
|
+
}
|
|
113543
113620
|
const spawned = readSpawnedHost(root);
|
|
113544
113621
|
if (spawned === null) {
|
|
113545
113622
|
console.error(
|
|
@@ -113572,21 +113649,18 @@ async function stopHost(root, hostUrl) {
|
|
|
113572
113649
|
}
|
|
113573
113650
|
const info2 = await getProcessInfo(spawned.pid);
|
|
113574
113651
|
if (info2 === null) {
|
|
113575
|
-
|
|
113576
|
-
|
|
113577
|
-
|
|
113578
|
-
|
|
113652
|
+
const res = await stopBackendServerPid(root, port);
|
|
113653
|
+
if (res === "failed") return 1;
|
|
113654
|
+
if (res !== "stopped") {
|
|
113655
|
+
console.log(import_picocolors36.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
|
|
113579
113656
|
}
|
|
113657
|
+
await cleanupSpawnedRecord(root);
|
|
113580
113658
|
return 0;
|
|
113581
113659
|
}
|
|
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)) {
|
|
113660
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113587
113661
|
console.error(
|
|
113588
113662
|
import_picocolors36.default.red(
|
|
113589
|
-
`Pid ${spawned.pid} args do not match "horus-source host --port ${
|
|
113663
|
+
`Pid ${spawned.pid} args do not match "horus-source host --port ${port}". Got: "${info2.args.slice(0, 120)}". Aborting for safety.`
|
|
113590
113664
|
)
|
|
113591
113665
|
);
|
|
113592
113666
|
return 1;
|
|
@@ -113639,11 +113713,57 @@ async function stopHost(root, hostUrl) {
|
|
|
113639
113713
|
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
113640
113714
|
);
|
|
113641
113715
|
}
|
|
113716
|
+
if (await stopBackendServerPid(root, port) === "failed") return 1;
|
|
113717
|
+
await cleanupSpawnedRecord(root);
|
|
113718
|
+
return 0;
|
|
113719
|
+
}
|
|
113720
|
+
function hostArgvRegex(port) {
|
|
113721
|
+
return new RegExp(
|
|
113722
|
+
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${String(port)}(?=\\s|$)`
|
|
113723
|
+
);
|
|
113724
|
+
}
|
|
113725
|
+
async function cleanupSpawnedRecord(root) {
|
|
113642
113726
|
try {
|
|
113643
113727
|
await unlinkAsync((0, import_node_path18.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
113644
113728
|
} catch {
|
|
113645
113729
|
}
|
|
113646
|
-
|
|
113730
|
+
}
|
|
113731
|
+
async function stopBackendServerPid(root, port) {
|
|
113732
|
+
const rec = readSourceHostPid(root);
|
|
113733
|
+
if (!rec) return "none";
|
|
113734
|
+
if (Number.isFinite(rec.port) && rec.port !== port) return "none";
|
|
113735
|
+
if (rec.repoPath && rec.repoPath !== root) return "none";
|
|
113736
|
+
const info2 = await getProcessInfo(rec.pid);
|
|
113737
|
+
if (info2 === null) return "none";
|
|
113738
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113739
|
+
console.error(
|
|
113740
|
+
import_picocolors36.default.red(
|
|
113741
|
+
`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.`
|
|
113742
|
+
)
|
|
113743
|
+
);
|
|
113744
|
+
return "failed";
|
|
113745
|
+
}
|
|
113746
|
+
try {
|
|
113747
|
+
process.kill(rec.pid, "SIGTERM");
|
|
113748
|
+
} catch (err2) {
|
|
113749
|
+
if (err2.code === "ESRCH") return "stopped";
|
|
113750
|
+
console.error(import_picocolors36.default.red(`Failed to signal pid ${rec.pid}: ${err2.message}`));
|
|
113751
|
+
return "failed";
|
|
113752
|
+
}
|
|
113753
|
+
const deadline = Date.now() + STOP_WAIT_MS;
|
|
113754
|
+
while (Date.now() < deadline) {
|
|
113755
|
+
await sleep(STOP_POLL_MS);
|
|
113756
|
+
if (await getProcessInfo(rec.pid) === null) {
|
|
113757
|
+
console.log(
|
|
113758
|
+
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${rec.pid}, port ${port})`) + ` for ${root}`
|
|
113759
|
+
);
|
|
113760
|
+
return "stopped";
|
|
113761
|
+
}
|
|
113762
|
+
}
|
|
113763
|
+
console.error(
|
|
113764
|
+
import_picocolors36.default.red(`Host pid ${rec.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`)
|
|
113765
|
+
);
|
|
113766
|
+
return "failed";
|
|
113647
113767
|
}
|
|
113648
113768
|
async function stopAll() {
|
|
113649
113769
|
const registry2 = readRegistry();
|
|
@@ -113657,8 +113777,10 @@ async function stopAll() {
|
|
|
113657
113777
|
for (const [name2, entry2] of projects2) {
|
|
113658
113778
|
const hostUrl = readSourceHostUrl(entry2.root);
|
|
113659
113779
|
if (!hostUrl) continue;
|
|
113660
|
-
const
|
|
113661
|
-
if (!
|
|
113780
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113781
|
+
if (!healthy && readSpawnedHost(entry2.root) === null && readSourceHostPid(entry2.root) === null) {
|
|
113782
|
+
continue;
|
|
113783
|
+
}
|
|
113662
113784
|
console.log(` Stopping ${import_picocolors36.default.bold(name2)} ${import_picocolors36.default.dim(`(${hostUrl})`)}`);
|
|
113663
113785
|
const code = await stopHost(entry2.root, hostUrl);
|
|
113664
113786
|
if (code === 0) stopped++;
|