crucix 2.39.3 → 2.39.5

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/README.md CHANGED
@@ -94,16 +94,31 @@ It was built for anyone who wants to understand what's actually happening in the
94
94
 
95
95
  ## Quick Start
96
96
 
97
+ ```powershell
98
+ # Windows: standalone exe from GitHub Releases (no Node)
99
+ irm https://github.com/arlinamid/Crucix/releases/latest/download/install.ps1 | iex
100
+ ```
101
+
102
+ ```bash
103
+ # Linux / macOS
104
+ curl -fsSL https://github.com/arlinamid/Crucix/releases/latest/download/install.sh | sh
105
+ ```
106
+
97
107
  ```bash
98
- # From npm (recommended): Node + runtime source, first run opens the setup wizard
108
+ # From npm: Node + runtime source, first run opens the setup wizard
99
109
  npx crucix
100
110
  npm install -g crucix
111
+ ```
112
+
113
+ Then:
114
+
115
+ ```bash
101
116
  crucix --version
102
117
  crucix install # ~/.crucix (.env and runs/)
103
118
  crucix # dashboard; wizard if no .env yet
104
119
  ```
105
120
 
106
- That is not the standalone exe. `npm run install:local` (from a git checkout) builds `crucix.exe` and copies it to `%USERPROFILE%\.local\bin` / `~/.local/bin`. Check which one PATH hits with `crucix --version`.
121
+ `npm run install:local` (from a git checkout) builds the exe yourself. Check which one PATH hits with `crucix --version`.
107
122
 
108
123
  Settings the wizard and **Settings → News feeds** save go to `%USERPROFILE%\.crucix` on Windows or `~/.crucix` on Linux/macOS. Set `CRUCIX_HOME` to choose another folder, or `CRUCIX_ENV_FILE` to name the `.env` itself.
109
124
 
@@ -10,6 +10,12 @@ export const RSS_TIMEOUT_MS = 12000;
10
10
  export const rssAgent = new Agent({
11
11
  connections: RSS_CONCURRENCY,
12
12
  pipelining: 0,
13
+ // Close the socket after each feed. Reusing a TLS connection that AbortSignal
14
+ // paused makes Node's HTTP parser throw assert(!this.paused) on socket end.
15
+ keepAliveTimeout: 1,
16
+ keepAliveMaxTimeout: 1,
17
+ bodyTimeout: RSS_TIMEOUT_MS,
18
+ headersTimeout: RSS_TIMEOUT_MS,
13
19
  connect: {
14
20
  timeout: 8000,
15
21
  family: 4,
@@ -0,0 +1,10 @@
1
+ // Node/undici can throw AssertionError `assert(!this.paused)` from Parser.finish when a
2
+ // TLS socket ends after AbortSignal paused the HTTP parser (RSS timeouts). The sweep
3
+ // already treated that feed as failed; the process must keep running.
4
+
5
+ export function isBenignHttpParserClose(error) {
6
+ if (!error || error.name !== 'AssertionError') return false;
7
+ const message = String(error.message || '');
8
+ const stack = String(error.stack || '');
9
+ return message.includes('assert(!this.paused)') && (stack.includes('Parser.finish') || stack.includes('onHttpSocketEnd'));
10
+ }
@@ -0,0 +1,81 @@
1
+ // One long-running Crucix (dashboard or --collector) per runs/ profile.
2
+ // A second copy would share AIS, daily quotas and the same latest.json.
3
+
4
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export const INSTANCE_LOCK_NAME = 'instance.lock';
8
+
9
+ export function instanceLockPath(runsDir) {
10
+ return join(runsDir, INSTANCE_LOCK_NAME);
11
+ }
12
+
13
+ export function processIsAlive(pid) {
14
+ if (!Number.isInteger(pid) || pid <= 0) return false;
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ } catch (error) {
19
+ return error?.code === 'EPERM';
20
+ }
21
+ }
22
+
23
+ export function acquireInstanceLock(runsDir, { pid = process.pid, now = Date.now() } = {}) {
24
+ if (!runsDir) throw new TypeError('runsDir is required');
25
+ mkdirSync(runsDir, { recursive: true });
26
+ const path = instanceLockPath(runsDir);
27
+ for (let attempt = 0; attempt < 2; attempt++) {
28
+ try {
29
+ const fd = openSync(path, 'wx');
30
+ try {
31
+ writeFileSync(fd, `${JSON.stringify({ pid, startedAt: new Date(now).toISOString() })}\n`);
32
+ } finally {
33
+ closeSync(fd);
34
+ }
35
+ return { path, pid, created: true };
36
+ } catch (error) {
37
+ if (error?.code !== 'EEXIST') throw error;
38
+ let other = null;
39
+ try { other = JSON.parse(readFileSync(path, 'utf8')); } catch { /* replace a corrupt lock */ }
40
+ if (Number(other?.pid) === pid) return { path, pid, created: false };
41
+ if (processIsAlive(Number(other?.pid))) {
42
+ return { error: 'already_running', pid: Number(other.pid), startedAt: other.startedAt || null, path };
43
+ }
44
+ try { unlinkSync(path); } catch { /* another process won the unlink */ }
45
+ }
46
+ }
47
+ return { error: 'already_running', path };
48
+ }
49
+
50
+ export function releaseInstanceLock(runsDir, { pid = process.pid } = {}) {
51
+ const path = instanceLockPath(runsDir);
52
+ if (!existsSync(path)) return false;
53
+ try {
54
+ const other = JSON.parse(readFileSync(path, 'utf8'));
55
+ if (Number(other?.pid) !== pid) return false;
56
+ unlinkSync(path);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ export function holdInstanceLock(runsDir, { exit = (code) => process.exit(code) } = {}) {
64
+ const lock = acquireInstanceLock(runsDir);
65
+ if (lock.error) return lock;
66
+ const release = () => { try { releaseInstanceLock(runsDir); } catch { /* process is leaving */ } };
67
+ process.on('exit', release);
68
+ for (const signal of ['SIGINT', 'SIGTERM']) {
69
+ process.on(signal, () => { release(); exit(0); });
70
+ }
71
+ return lock;
72
+ }
73
+
74
+ export function alreadyRunningMessage(lock, { port, lang = 'en' } = {}) {
75
+ const pid = lock?.pid ? ` (pid ${lock.pid})` : '';
76
+ const where = port ? ` http://localhost:${port}` : '';
77
+ if (lang === 'hu') {
78
+ return `[Crucix] FATAL: már fut egy példány${pid}.${where ? ` Dashboard:${where}` : ''} Állítsd le azt, mielőtt újat indítasz.`;
79
+ }
80
+ return `[Crucix] FATAL: already running${pid}.${where ? ` Dashboard:${where}` : ''} Stop that instance before starting another.`;
81
+ }
@@ -0,0 +1,32 @@
1
+ // Names of the GitHub Release assets that `scripts/build-exe.mjs` and CI upload.
2
+ // install.ps1 / install.sh must stay in lockstep with this map.
3
+
4
+ const OS = Object.freeze({ win32: 'windows', linux: 'linux', darwin: 'macos' });
5
+ const ARCH = Object.freeze({ x64: 'x64', arm64: 'arm64' });
6
+
7
+ export function releaseAssetName(platform = process.platform, arch = process.arch) {
8
+ const os = OS[platform];
9
+ const cpu = ARCH[arch];
10
+ if (!os || !cpu) return null;
11
+ return `crucix-${os}-${cpu}${os === 'windows' ? '.exe' : ''}`;
12
+ }
13
+
14
+ export function installedBinaryName(platform = process.platform) {
15
+ return platform === 'win32' ? 'crucix.exe' : 'crucix';
16
+ }
17
+
18
+ export function checksumFor(sumsText, filename) {
19
+ if (!sumsText || !filename) return null;
20
+ for (const line of String(sumsText).split(/\r?\n/)) {
21
+ const match = line.match(/^([a-fA-F0-9]{64})\s+(\S+)\s*$/);
22
+ if (match && match[2] === filename) return match[1].toLowerCase();
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function releaseDownloadUrl(repo, tag, filename) {
28
+ const ownerRepo = repo || 'arlinamid/Crucix';
29
+ const ref = !tag || tag === 'latest' ? 'latest' : tag;
30
+ const path = ref === 'latest' ? 'latest/download' : `download/${ref}`;
31
+ return `https://github.com/${ownerRepo}/releases/${path}/${filename}`;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crucix",
3
- "version": "2.39.3",
3
+ "version": "2.39.5",
4
4
  "description": "Local intelligence engine - 52 OSINT adapters, live dashboard, bounded requests, optional local or cloud AI.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/cli.mjs CHANGED
@@ -22,6 +22,7 @@ import { getEnvSearchPaths } from '../apis/utils/env.mjs';
22
22
  import { installUserProfile } from '../lib/profile-install.mjs';
23
23
  import { resolveRunsDir as resolveUserRunsDir } from '../lib/user-paths.mjs';
24
24
  import { packageVersion, versionLine } from '../lib/version.mjs';
25
+ import { alreadyRunningMessage, holdInstanceLock } from '../lib/instance-lock.mjs';
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
28
  const ROOT = resolve(__dirname, '..');
@@ -1578,6 +1579,11 @@ async function main() {
1578
1579
 
1579
1580
  // 0b. Continuous data collector loop without any web server
1580
1581
  if (opts.collector) {
1582
+ const lock = holdInstanceLock(runsDir);
1583
+ if (lock.error) {
1584
+ console.error(alreadyRunningMessage(lock, { port, lang }));
1585
+ process.exit(1);
1586
+ }
1581
1587
  const intervalMinutes = opts.interval || 15;
1582
1588
  if (lang === 'hu') {
1583
1589
  console.log('\n🔄 Crucix Folyamatos Adatgyűjtő Ciklus (Headless Collector)');
@@ -0,0 +1,76 @@
1
+ # Crucix standalone installer (Windows x64). Node is not required.
2
+ # irm https://github.com/arlinamid/Crucix/releases/latest/download/install.ps1 | iex
3
+ # Pin a tag: $env:CRUCIX_VERSION = 'v2.39.3'; irm ... | iex
4
+ # Dest: $env:CRUCIX_BIN (default %USERPROFILE%\.local\bin)
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ $repo = if ($env:CRUCIX_REPO) { $env:CRUCIX_REPO } else { 'arlinamid/Crucix' }
8
+ $tag = if ($env:CRUCIX_VERSION) { $env:CRUCIX_VERSION } else { 'latest' }
9
+ $binDir = if ($env:CRUCIX_BIN) { $env:CRUCIX_BIN } else { Join-Path $env:USERPROFILE '.local\bin' }
10
+
11
+ $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
12
+ if ($arch -eq 'AMD64') { $cpu = 'x64' }
13
+ elseif ($arch -eq 'ARM64') { $cpu = 'arm64' }
14
+ else { throw "Unsupported Windows architecture: $arch" }
15
+
16
+ $asset = "crucix-windows-$cpu.exe"
17
+ if ($cpu -ne 'x64') {
18
+ throw "This release has no Windows $cpu binary yet. Use an x64 machine or build from source (npm run install:local)."
19
+ }
20
+
21
+ $base = if ($tag -eq 'latest') {
22
+ "https://github.com/$repo/releases/latest/download"
23
+ } else {
24
+ if ($tag -notmatch '^v') { $tag = "v$tag" }
25
+ "https://github.com/$repo/releases/download/$tag"
26
+ }
27
+
28
+ $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("crucix-dl-" + [guid]::NewGuid().ToString('n'))
29
+ New-Item -ItemType Directory -Force -Path $tmp | Out-Null
30
+ $exeTmp = Join-Path $tmp $asset
31
+ $sumsTmp = Join-Path $tmp 'SHA256SUMS.txt'
32
+
33
+ Write-Host "Crucix standalone install"
34
+ Write-Host " source $base/$asset"
35
+ Write-Host " dest $(Join-Path $binDir 'crucix.exe')"
36
+
37
+ try {
38
+ try {
39
+ Invoke-WebRequest -Uri "$base/$asset" -OutFile $exeTmp -UseBasicParsing
40
+ Invoke-WebRequest -Uri "$base/SHA256SUMS.txt" -OutFile $sumsTmp -UseBasicParsing
41
+ } catch {
42
+ throw "Download failed. Check $base/$asset ($($_.Exception.Message))"
43
+ }
44
+
45
+ $actual = (Get-FileHash -Algorithm SHA256 -Path $exeTmp).Hash.ToLowerInvariant()
46
+ $expected = $null
47
+ foreach ($line in Get-Content $sumsTmp) {
48
+ if ($line -match '^([a-fA-F0-9]{64})\s+(\S+)\s*$' -and $Matches[2] -eq $asset) {
49
+ $expected = $Matches[1].ToLowerInvariant()
50
+ break
51
+ }
52
+ }
53
+ if (-not $expected) { throw "SHA256SUMS.txt has no entry for $asset" }
54
+ if ($actual -ne $expected) { throw "Checksum mismatch for $asset`n expected $expected`n got $actual" }
55
+ Write-Host " sha256 $actual"
56
+
57
+ New-Item -ItemType Directory -Force -Path $binDir | Out-Null
58
+ $target = Join-Path $binDir 'crucix.exe'
59
+ Copy-Item -Force $exeTmp $target
60
+ Set-Content -Path (Join-Path $binDir 'crucix.cmd') -Value "@echo off`r`n`"%~dp0crucix.exe`" %*`r`n" -Encoding ascii
61
+ $sh = "#!/usr/bin/env sh`nbasedir=`$(dirname `"`$0`")`nexec `"`$basedir/crucix.exe`" `"`$@`"`n"
62
+ Set-Content -Path (Join-Path $binDir 'crucix') -Value $sh -Encoding ascii
63
+
64
+ $onPath = ($env:PATH -split ';' | ForEach-Object { $_.TrimEnd('\') }) -contains $binDir.TrimEnd('\')
65
+ Write-Host ""
66
+ Write-Host "Installed $target"
67
+ if (-not $onPath) {
68
+ Write-Host "Add this folder to PATH, then open a new terminal:"
69
+ Write-Host " $binDir"
70
+ }
71
+ Write-Host " crucix --version"
72
+ Write-Host " crucix install --no-setup"
73
+ Write-Host " crucix setup"
74
+ } finally {
75
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
76
+ }
@@ -0,0 +1,80 @@
1
+ #!/bin/sh
2
+ # Crucix standalone installer (Linux x64, macOS arm64). Node is not required.
3
+ # curl -fsSL https://github.com/arlinamid/Crucix/releases/latest/download/install.sh | sh
4
+ # Pin a tag: CRUCIX_VERSION=v2.39.3 curl -fsSL ... | sh
5
+ # Dest: CRUCIX_BIN (default ~/.local/bin)
6
+
7
+ set -eu
8
+ REPO="${CRUCIX_REPO:-arlinamid/Crucix}"
9
+ TAG="${CRUCIX_VERSION:-latest}"
10
+ BIN_DIR="${CRUCIX_BIN:-$HOME/.local/bin}"
11
+
12
+ os=$(uname -s)
13
+ arch=$(uname -m)
14
+ case "$os" in
15
+ Linux) os_name=linux ;;
16
+ Darwin) os_name=macos ;;
17
+ *) echo "Unsupported OS: $os" >&2; exit 1 ;;
18
+ esac
19
+ case "$arch" in
20
+ x86_64|amd64) cpu=x64 ;;
21
+ arm64|aarch64) cpu=arm64 ;;
22
+ *) echo "Unsupported architecture: $arch" >&2; exit 1 ;;
23
+ esac
24
+
25
+ ASSET="crucix-${os_name}-${cpu}"
26
+ if [ "$TAG" != latest ] && ! echo "$TAG" | grep -q '^v'; then
27
+ TAG="v$TAG"
28
+ fi
29
+ if [ "$TAG" = latest ]; then
30
+ BASE="https://github.com/${REPO}/releases/latest/download"
31
+ else
32
+ BASE="https://github.com/${REPO}/releases/download/${TAG}"
33
+ fi
34
+
35
+ TMP=$(mktemp -d)
36
+ trap 'rm -rf "$TMP"' EXIT
37
+ echo "Crucix standalone install"
38
+ echo " source ${BASE}/${ASSET}"
39
+ echo " dest ${BIN_DIR}/crucix"
40
+
41
+ if ! curl -fsSL "${BASE}/${ASSET}" -o "${TMP}/${ASSET}"; then
42
+ echo "Download failed. This release may not have ${ASSET}." >&2
43
+ exit 1
44
+ fi
45
+ curl -fsSL "${BASE}/SHA256SUMS.txt" -o "${TMP}/SHA256SUMS.txt"
46
+
47
+ if command -v sha256sum >/dev/null 2>&1; then
48
+ ACTUAL=$(sha256sum "${TMP}/${ASSET}" | awk '{print $1}')
49
+ elif command -v shasum >/dev/null 2>&1; then
50
+ ACTUAL=$(shasum -a 256 "${TMP}/${ASSET}" | awk '{print $1}')
51
+ else
52
+ echo "Need sha256sum or shasum to verify the download." >&2
53
+ exit 1
54
+ fi
55
+ EXPECTED=$(awk -v f="$ASSET" '$2==f {print $1; exit}' "${TMP}/SHA256SUMS.txt" | tr 'A-F' 'a-f')
56
+ if [ -z "$EXPECTED" ]; then
57
+ echo "SHA256SUMS.txt has no entry for ${ASSET}" >&2
58
+ exit 1
59
+ fi
60
+ if [ "$ACTUAL" != "$EXPECTED" ]; then
61
+ echo "Checksum mismatch for ${ASSET}" >&2
62
+ echo " expected ${EXPECTED}" >&2
63
+ echo " got ${ACTUAL}" >&2
64
+ exit 1
65
+ fi
66
+ echo " sha256 ${ACTUAL}"
67
+
68
+ mkdir -p "$BIN_DIR"
69
+ cp "${TMP}/${ASSET}" "${BIN_DIR}/crucix"
70
+ chmod 755 "${BIN_DIR}/crucix"
71
+
72
+ echo
73
+ echo "Installed ${BIN_DIR}/crucix"
74
+ case ":$PATH:" in
75
+ *":${BIN_DIR}:"*) ;;
76
+ *) echo "Add this folder to PATH, then open a new terminal:"; echo " ${BIN_DIR}" ;;
77
+ esac
78
+ echo " crucix --version"
79
+ echo " crucix install --no-setup"
80
+ echo " crucix setup"
package/server.mjs CHANGED
@@ -54,6 +54,8 @@ import { openFeedStore, setActiveFeedStore } from './lib/news/feed-store.mjs';
54
54
  import { installFeedRoutes } from './lib/news/routes.mjs';
55
55
  import { resolveRunsDir as resolveUserRunsDir, feedsSettingsPath, settingsEnvPath } from './lib/user-paths.mjs';
56
56
  import { resolveDashboardPublicDir, readJarvisHtml } from './lib/dashboard-path.mjs';
57
+ import { alreadyRunningMessage, holdInstanceLock } from './lib/instance-lock.mjs';
58
+ import { isBenignHttpParserClose } from './lib/http-parser-error.mjs';
57
59
 
58
60
  const __dirname = dirname(fileURLToPath(import.meta.url));
59
61
  const ROOT = __dirname;
@@ -579,6 +581,12 @@ async function runSweepCycle() {
579
581
 
580
582
  // === Startup ===
581
583
  async function start() {
584
+ const lock = holdInstanceLock(RUNS_DIR);
585
+ if (lock.error) {
586
+ console.error(alreadyRunningMessage(lock, { port: config.port, lang: currentLanguage }));
587
+ process.exit(1);
588
+ }
589
+
582
590
  const port = config.port;
583
591
 
584
592
  const isHeadless =
@@ -672,7 +680,15 @@ async function start() {
672
680
  process.on('unhandledRejection', (err) => {
673
681
  console.error('[Crucix] Unhandled rejection:', err?.stack || err?.message || err);
674
682
  });
683
+ let httpParserCloseWarned = false;
675
684
  process.on('uncaughtException', (err) => {
685
+ if (isBenignHttpParserClose(err)) {
686
+ if (!httpParserCloseWarned) {
687
+ httpParserCloseWarned = true;
688
+ console.warn('[Crucix] Node HTTP parser hit a paused TLS socket (RSS abort). Continuing; this is not a sweep failure.');
689
+ }
690
+ return;
691
+ }
676
692
  console.error('[Crucix] Uncaught exception:', err?.stack || err?.message || err);
677
693
  });
678
694