clearotron 0.3.0-beta.8 → 0.3.0

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.
@@ -34,6 +34,30 @@
34
34
  * like an engine fault. Names here must be values this code MINTS; putting
35
35
  * a credential a reader supplied in this list would delete their work.
36
36
  */
37
+ // ── THE ONE `KEY=value` READER, AND IT LIVES IN A LEAF ────────────────────────────────────────────
38
+ //
39
+ // This reads the same file systemd's EnvironmentFile= reads, and systemd does no shell expansion there
40
+ // either. Quotes are stripped because operators write them.
41
+ //
42
+ // IT USED TO LIVE IN `driver/systemd/render-units.mjs`, which is a COMMAND as well as a module, and its
43
+ // `--apply` path awaits at the top level and imports `bin/start.mjs` from inside that await. So a reader
44
+ // that took the parser from there put the command's own module in its import path: `start.mjs` importing
45
+ // it closed a cycle, the CLI's top-level await never settled, and 21 install arms died at once with
46
+ // "Detected unsettled top-level await" and nothing naming the cause (measured 2026-09-12). The parser has
47
+ // no business depending on any of that — it is eleven lines of string handling — so it sits here, in a
48
+ // file that imports nothing, and `render-units.mjs` re-exports it so its readers keep one reader.
49
+ export function parseEnvFile(text) {
50
+ const out = {};
51
+ for (const line of String(text ?? "").split("\n")) {
52
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
53
+ if (!m) continue;
54
+ let v = m[2].trim();
55
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
56
+ out[m[1]] = v;
57
+ }
58
+ return out;
59
+ }
60
+
37
61
  export function mergeEnvFile(text, additions, { by = "`npm start` (bin/start.mjs)", notes = {}, refresh = [] } = {}) {
38
62
  const body = typeof text === "string" ? text : "";
39
63
  const present = new Set();
@@ -205,6 +205,24 @@ export function invocationForm(env = process.env, io = FS, installDir = INSTALL_
205
205
  // right there and naming it in full is true from any directory.
206
206
  return { form: "shim-path", prefix: `${globalDir}${sep}`, shim: globalExe, dir: globalDir, onPath, shadowedBy, staleInterpreter: null, via: "global" };
207
207
  }
208
+ // ── FROM NPX'S CACHE, THE PUBLISHED VERSION, NOT THE CACHE ──────────────────────────────────────────
209
+ //
210
+ // `in-place` below names the directory this install runs from, and under npx that is npm's cache:
211
+ // `cd <npm's cache> && npx clearotron …`. npm deletes that directory when it cleans its cache, so every
212
+ // command a verb printed there stopped working before its reader typed it (a packaged-install drive of
213
+ // `doctor`, 2026-09-11, found thirteen). `npx -p clearotron@<version>` fetches this same version from any
214
+ // directory. It is the decision `browserCommand` makes for a page, `npx clearotron@<version> <verb>`,
215
+ // spelled for a PREFIX, which the word `clearotron` follows. Asked after a shim of ours and a global
216
+ // install, which are stronger evidence than the cache path; a version that cannot be read falls through
217
+ // to `in-place` rather than naming a guess.
218
+ const npxVersion = npxVersionOf(installDir, io.read ?? readFileSync);
219
+ if (npxVersion) {
220
+ return {
221
+ form: "npx-pinned", prefix: `npx -p clearotron@${npxVersion} `, shim: path, dir,
222
+ onPath: false, shadowedBy: null, shimKind: shim.kind, otherInstall: shim.installDir,
223
+ staleInterpreter: shim.interpreterMissing === true ? shim.interpreter : null,
224
+ };
225
+ }
208
226
  // ✕ A BROKEN SHIM MUST NOT BE THE FORM WE HAND BACK, however well it identifies itself. Driven and
209
227
  // caught: doctor reported the stale interpreter and then told the reader to run `clearotron install`
210
228
  // to fix it — through the very shim it had just called broken. The advice for repairing a route
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // running-start.mjs — which foreground `clearotron start` processes are serving on this machine, and where.
5
+ //
6
+ // `status` promises "is the product up, and on which ports", and knew only about background units. With
7
+ // the product running the way the README starts it — `clearotron start` in a terminal — it printed a
8
+ // sentence about units the reader never installed, and `stop` said nothing was running while the portal
9
+ // answered 200 (measured on a published beta, 2026-09-11). A foreground start now leaves one small record
10
+ // here while it serves, and both verbs read it.
11
+ //
12
+ // THE RECORD IS AN ADDRESS, NEVER PROOF OF LIFE. A start killed outright leaves its record behind, so a
13
+ // record whose process is gone is read as absent, and `status` asks the portal itself before saying the
14
+ // product is up. Three answers, all honest: up; started but not answering; not running.
15
+
16
+ import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ /**
21
+ * Where the records live: one file per serving process, beside the settings and the revocation list.
22
+ *
23
+ * `~/.config/clearotron` is this product's per-user directory and the only one it reads. An XDG state
24
+ * directory would be the tidier home for this, and it would be a NEW environment variable read by product
25
+ * code — a thing this repo documents in two contract files and classifies in another repo. Not worth a
26
+ * paired change for a file the reader never opens.
27
+ */
28
+ export function runningDir({ home = homedir() } = {}) {
29
+ return join(home, ".config", "clearotron", "running");
30
+ }
31
+
32
+ /** Is this process alive? EPERM means it exists and belongs to somebody else, which is alive. */
33
+ export function pidAlive(pid) {
34
+ try { process.kill(pid, 0); return true; } catch (e) { return e?.code === "EPERM"; }
35
+ }
36
+
37
+ /**
38
+ * Record one serving start. Returns the function that removes the record; calling it twice is harmless,
39
+ * so the caller can hang it on both its own shutdown and the process's `exit`.
40
+ */
41
+ export function recordRunning(rec, { dir = runningDir() } = {}) {
42
+ mkdirSync(dir, { recursive: true });
43
+ const file = join(dir, `${rec.pid}.json`);
44
+ const tmp = `${file}.tmp`;
45
+ writeFileSync(tmp, `${JSON.stringify(rec, null, 2)}\n`, { mode: 0o600 });
46
+ renameSync(tmp, file);
47
+ let gone = false;
48
+ return () => {
49
+ if (gone) return;
50
+ gone = true;
51
+ try { rmSync(file, { force: true }); } catch { /* already gone */ }
52
+ };
53
+ }
54
+
55
+ /** Every record whose process is alive, oldest pid first. Reads; removes nothing. */
56
+ export function readRunning({ dir = runningDir(), alive = pidAlive } = {}) {
57
+ let names;
58
+ try { names = readdirSync(dir); } catch { return []; }
59
+ const out = [];
60
+ for (const name of names.filter((n) => /^\d+\.json$/.test(n))) {
61
+ let rec;
62
+ try { rec = JSON.parse(readFileSync(join(dir, name), "utf8")); } catch { continue; }
63
+ if (!Number.isInteger(rec?.pid) || typeof rec?.url !== "string" || !rec?.ports) continue;
64
+ if (!alive(rec.pid)) continue; // a start that did not exit cleanly: its record says nothing now
65
+ out.push(rec);
66
+ }
67
+ return out.sort((a, b) => a.pid - b.pid);
68
+ }
69
+
70
+ /** Does the portal at `url` answer? A timeout or a refusal is "no", never "unknown dressed as yes". */
71
+ export async function probe(url, { timeoutMs = 2000 } = {}) {
72
+ try { return (await fetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok; } catch { return false; }
73
+ }