clearotron 0.3.0-beta.10 → 0.3.0-beta.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/.env.example +3 -13
- package/INSTALL.md +6 -110
- package/README.md +21 -31
- package/bin/connect.mjs +26 -71
- package/bin/disconnect.mjs +11 -16
- package/bin/example.mjs +2 -14
- package/bin/key.mjs +5 -23
- package/bin/onboard.mjs +44 -191
- package/bin/start.mjs +33 -197
- package/bin/status.mjs +3 -23
- package/bin/stop.mjs +8 -23
- package/bin/update.mjs +6 -38
- package/build-info.json +2 -2
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/email-body.md +1 -1
- package/demo/knockout-search/run/status.json +2 -2
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/architecture/04-configuration-reference.md +1 -2
- package/driver/CHANGELOG.md +8 -107
- package/driver/compose-read.mjs +2 -21
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +2 -65
- package/driver/driver.config.mjs +0 -5
- package/driver/engine/anthropic-agent.mjs +17 -34
- package/driver/engine/jx-turn.mjs +1 -4
- package/driver/engine/openai-agent.mjs +2 -60
- package/driver/enqueue-schema.mjs +1 -3
- package/driver/gateway.mjs +15 -88
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +3 -35
- package/driver/portal-service.mjs +22 -91
- package/driver/portal-upstream.mjs +1 -20
- package/driver/profile-service.mjs +3 -31
- package/driver/publish/index.mjs +9 -12
- package/driver/publish/knockout.mjs +2 -4
- package/driver/publish/office-record-links.mjs +21 -56
- package/driver/recipe-service.mjs +5 -14
- package/driver/record-origins.mjs +0 -14
- package/driver/search-policy.mjs +2 -7
- package/driver/suite-census.json +46 -154
- package/driver/systemd/render-units.mjs +13 -7
- package/mcp-server/CHANGELOG.md +2 -36
- package/mcp-server/CONNECT.md +2 -3
- package/mcp-server/lib/driver.mjs +0 -2
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +5 -20
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CwPAS0we.js → index-CcFjgM78.js} +383 -525
- package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-CsCuPshD.css} +86 -199
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +0 -5
- package/providers/oauth-mcp-bridge/CHANGELOG.md +0 -32
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +20 -197
- package/scripts/engine-probe.mjs +2 -2
- package/scripts/env-classify.mjs +0 -3
- package/scripts/revisit-render-check.mjs +4 -22
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/access-audience.mjs +6 -7
- package/shared/brand.mjs +3 -7
- package/shared/client-door.mjs +0 -39
- package/shared/connect-clients.mjs +321 -290
- package/shared/env-file-merge.mjs +0 -24
- package/shared/invocation.mjs +2 -49
- package/shared/names-in-force.mjs +0 -2
- package/shared/stdio-connect.mjs +14 -95
- package/shared/store-in-repo.mjs +2 -50
- package/shared/verb-shim.mjs +1 -10
- package/shared/permanent-install.mjs +0 -237
- package/shared/running-start.mjs +0 -73
- package/shared/wsl.mjs +0 -23
package/bin/onboard.mjs
CHANGED
|
@@ -63,13 +63,11 @@ import { createInterface } from "node:readline/promises";
|
|
|
63
63
|
import { stdin as input, stdout as output } from "node:process";
|
|
64
64
|
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, chmodSync } from "node:fs"; // read the process table here; moved that to shared/process-table.mjs
|
|
65
65
|
import { homedir, userInfo } from "node:os";
|
|
66
|
-
import { invocationPrefix
|
|
66
|
+
import { invocationPrefix } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
|
|
67
67
|
import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor is package.json engines, not a constant here
|
|
68
68
|
import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
|
|
69
69
|
import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
|
|
70
70
|
import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
|
|
71
|
-
import { relocationPlan } from "../shared/permanent-install.mjs"; // — and the program out of npx's cache
|
|
72
|
-
import { isWsl } from "../shared/wsl.mjs"; // — one answer to "is this WSL", shared with the connect lines
|
|
73
71
|
import { styleFor, banner } from "../shared/tty-style.mjs"; // — weight where the meaning is
|
|
74
72
|
import { bracketAsciiCells, BRAND } from "../shared/brand.mjs"; // F18 — the mark, from the geometry the SVG already uses
|
|
75
73
|
// THE REFUSALS ABOUT THE SIGN-IN ADDRESS ITSELF, shared with `bin/start.mjs`. Two copies would be a
|
|
@@ -99,17 +97,9 @@ import { processTable } from "../shared/process-table.mjs"; // — /proc is no
|
|
|
99
97
|
import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
|
|
100
98
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // one ExecStart parser
|
|
101
99
|
import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
|
|
102
|
-
import { whereSavesGo
|
|
100
|
+
import { whereSavesGo } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed
|
|
103
101
|
import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
|
|
104
|
-
import { probeEngineTurn, probeFailureText, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
105
|
-
|
|
106
|
-
// THE PROVING SENTENCES NAME NO MODEL. They printed the driver's tier word, which is an Anthropic model's
|
|
107
|
-
// name, on both engines, so a codex user was told setup was about to spend on a model family they do not
|
|
108
|
-
// use. "Its cheapest model" is true of either engine and brands neither.
|
|
109
|
-
export const probingLine = (engineId) =>
|
|
110
|
-
`Probing ${engineId} with one turn on its cheapest model (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`;
|
|
111
|
-
export const proveQuestion = ({ engineId, lane }) =>
|
|
112
|
-
`Prove ${engineId} on the ${lane} lane now with one turn on its cheapest model (a few tokens, ${PROBE_TIMEOUT_SEC}s ceiling)?`;
|
|
102
|
+
import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
113
103
|
import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
|
|
114
104
|
import { pinEnv, envFrom } from "../shared/env-aliases.mjs";
|
|
115
105
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
@@ -130,12 +120,6 @@ function readIfPresent(path) {
|
|
|
130
120
|
}
|
|
131
121
|
|
|
132
122
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
133
|
-
// A PACKAGED INSTALL IS NOT A CHECKOUT. doctor says which one it is talking to, and names only commands
|
|
134
|
-
// that run there: npm's scripts and a checkout's tree are not where a package's reader stands.
|
|
135
|
-
const PACKAGED = installRoute(REPO) === "packaged";
|
|
136
|
-
const THIS_TREE = PACKAGED ? "the installed package" : "this checkout";
|
|
137
|
-
// The example job, by the path it has in THIS install, so the printed command runs from any directory.
|
|
138
|
-
const EXAMPLE_JOB = join(REPO, "examples", "job.euipo.json");
|
|
139
123
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
140
124
|
// WHAT IS READ IS NOT ALWAYS WHERE THE NEXT WRITE GOES. An install configured before the move still
|
|
141
125
|
// has its file at the old path, and the loader still reads it — so every READ here asks
|
|
@@ -711,8 +695,23 @@ export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindows
|
|
|
711
695
|
/** A path on a Windows drive as WSL mounts it. */
|
|
712
696
|
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
713
697
|
|
|
714
|
-
/**
|
|
715
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Whether this is a Linux running under Windows.
|
|
700
|
+
*
|
|
701
|
+
* BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects
|
|
702
|
+
* are the ones who cannot run this suite to find out, so a Linux runner has to be able to drive both
|
|
703
|
+
* answers rather than read the source and agree with it.
|
|
704
|
+
*
|
|
705
|
+
* A READ THAT FAILS ANSWERS "NOT WSL", and that is the direction that changes nothing: it leaves the
|
|
706
|
+
* resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start
|
|
707
|
+
* refusing candidates under /mnt on an ordinary Linux box with an ordinary mount.
|
|
708
|
+
*/
|
|
709
|
+
export function isWsl({ env = process.env, procVersion = null } = {}) {
|
|
710
|
+
if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
|
|
711
|
+
if (String(env.WSL_INTEROP ?? "").trim()) return true;
|
|
712
|
+
const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
|
|
713
|
+
return /microsoft|wsl/i.test(v);
|
|
714
|
+
}
|
|
716
715
|
|
|
717
716
|
/**
|
|
718
717
|
* What to say about candidates passed over because they sit on a Windows drive — or `null` when none
|
|
@@ -1111,10 +1110,7 @@ export async function runCheck() {
|
|
|
1111
1110
|
say(" Restart whatever supervises them; this command does not, because which supervisor owns");
|
|
1112
1111
|
say(" them is a property of your deployment and not of this checkout.");
|
|
1113
1112
|
} else if (running.state === "unknown") {
|
|
1114
|
-
|
|
1115
|
-
// question belongs to a checkout, and on a package it is a `!` nothing can ever clear.
|
|
1116
|
-
if (PACKAGED) info("running programs against a checkout's tree: not applicable to a packaged install");
|
|
1117
|
-
else warn(`could not tell whether running programs are on the current tree: ${running.detail}`);
|
|
1113
|
+
warn(`could not tell whether running programs are on the current tree: ${running.detail}`);
|
|
1118
1114
|
}
|
|
1119
1115
|
|
|
1120
1116
|
// ── AND THE MORE DANGEROUS ANSWER: A DIFFERENT TREE, NOT AN OLDER ONE ──────────────────────────────
|
|
@@ -1146,8 +1142,7 @@ export async function runCheck() {
|
|
|
1146
1142
|
if (elsewhere.programs.length > 6) say(` … and ${elsewhere.programs.length - 6} more`);
|
|
1147
1143
|
say(" Either point the install back at the tree they run from, or restart them onto this one.");
|
|
1148
1144
|
} else if (elsewhere.state === "unknown") {
|
|
1149
|
-
|
|
1150
|
-
else warn(`could not tell whether running programs are on a different checkout: ${elsewhere.detail}`);
|
|
1145
|
+
warn(`could not tell whether running programs are on a different checkout: ${elsewhere.detail}`);
|
|
1151
1146
|
} else if (elsewhere.state === "unplaced") {
|
|
1152
1147
|
// PRINTED, because the alternative is silence that reads as a clean answer. Nothing here was placed
|
|
1153
1148
|
// on a tree, which is not the same as everything being on the right one, and the reader is the only
|
|
@@ -1211,7 +1206,7 @@ export async function runCheck() {
|
|
|
1211
1206
|
// deliberate — a second reader would drift from this one exactly as the composer and the checker did
|
|
1212
1207
|
// in F41, and the drift is invisible because both sides keep passing their own arms.
|
|
1213
1208
|
const unitDir = join(homedir(), ".config", "systemd", "user");
|
|
1214
|
-
const { BACKGROUND_UNITS
|
|
1209
|
+
const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
|
|
1215
1210
|
const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
|
|
1216
1211
|
const unitEnv = hosted
|
|
1217
1212
|
? unitEnvironment({
|
|
@@ -1353,7 +1348,7 @@ export async function runCheck() {
|
|
|
1353
1348
|
const installMode = engineMode(engineInventory(invEnv));
|
|
1354
1349
|
if (installMode === ENGINE_MODES.DEMO) {
|
|
1355
1350
|
info("MODE: demo — everything works except starting a NEW search. The example report, its audit trail "
|
|
1356
|
-
+
|
|
1351
|
+
+ "and the MCP connection are live right now; `npm run example` needs no engine.");
|
|
1357
1352
|
// NAMES THE COMMAND, AND THE RESTART. The settings page says both now, and a doctor that named
|
|
1358
1353
|
// the program but not how to install it — or that left out the restart, which is what actually
|
|
1359
1354
|
// unsticks a reader who has just installed it — would be the third opinion this issue exists to
|
|
@@ -1445,7 +1440,7 @@ export async function runCheck() {
|
|
|
1445
1440
|
} else if (!bin.executable || bin.relative) {
|
|
1446
1441
|
info("not probed — there is no usable binary to probe. Fix the line above first.");
|
|
1447
1442
|
} else {
|
|
1448
|
-
say(`\n ${
|
|
1443
|
+
say(`\n Probing ${engineId} with one ${PROBE_MODEL}-tier turn (this SPENDS; ${PROBE_TIMEOUT_SEC}s ceiling)…`);
|
|
1449
1444
|
// The engine as a RUN would see it: environment first, .env behind it. Only the engine-selection
|
|
1450
1445
|
// keys — the probe must bill exactly the way this box bills and moves no other variable.
|
|
1451
1446
|
const probeEnv = { ...process.env };
|
|
@@ -1503,9 +1498,6 @@ export async function runCheck() {
|
|
|
1503
1498
|
} else if (form.form === "shim-path") {
|
|
1504
1499
|
warn(`${form.dir} is not on this shell's PATH, so the bare \`clearotron\` will not resolve here`);
|
|
1505
1500
|
info(`add it with: export PATH="${form.dir}:$PATH" — or open a new login shell`);
|
|
1506
|
-
} else if (form.form === "npx-pinned") {
|
|
1507
|
-
info(`this is running from npm's npx cache, so the commands below name this version through npx (\`${form.prefix.trim()}\`), `
|
|
1508
|
-
+ `which works from any directory and after npm cleans its cache; \`${invoke("install")}\` puts \`clearotron\` on your PATH`);
|
|
1509
1501
|
} else if (form.shimKind === "ours-other-install") {
|
|
1510
1502
|
warn(`${form.shim} is a shim for a DIFFERENT install (${form.otherInstall})`);
|
|
1511
1503
|
info(`re-run \`${invoke("install")}\` to point the bare name at this one`);
|
|
@@ -1561,9 +1553,9 @@ export async function runCheck() {
|
|
|
1561
1553
|
{
|
|
1562
1554
|
const inCheckout = (p) => isInsideCheckout(p, REPO);
|
|
1563
1555
|
for (const [name, what] of [
|
|
1564
|
-
["CLEAROTRON_CUSTOMERS_DIR",
|
|
1565
|
-
["CLEAROTRON_INSTRUCTIONS_DIR",
|
|
1566
|
-
["PROFILE_REPO_ROOT",
|
|
1556
|
+
["CLEAROTRON_CUSTOMERS_DIR", "customers resolve to the bundled demo roster IN this checkout"],
|
|
1557
|
+
["CLEAROTRON_INSTRUCTIONS_DIR", "doctrine resolves to the bundled files IN this checkout"],
|
|
1558
|
+
["PROFILE_REPO_ROOT", "the portal's profile editor commits INTO this checkout"],
|
|
1567
1559
|
]) {
|
|
1568
1560
|
// — WHICH NAME THE READER IS TOLD IS NOT ONE QUESTION, IT IS TWO.
|
|
1569
1561
|
//
|
|
@@ -1636,13 +1628,6 @@ export async function runCheck() {
|
|
|
1636
1628
|
// because a reader debugging `clearotron run` by hand IS this process.
|
|
1637
1629
|
info(`this command's own process ${where} — a CLI is not started by the units' EnvironmentFile, so `
|
|
1638
1630
|
+ "that differs by design and is not what a run uses");
|
|
1639
|
-
} else if (!hosted && svcCustomers?.from === serviceEnvLabel) {
|
|
1640
|
-
// NAMED IN THE ENV FILE ALONE, ON AN INSTALL WITH NO UNITS. `clearotron start` loads that file and
|
|
1641
|
-
// its services inherit the store; this command does not load it, so its own process fell back to
|
|
1642
|
-
// the bundled roster and printed that as the answer, one line below a ✓ for the same variable.
|
|
1643
|
-
ok(`the services resolve profiles from ${svcCustomers.v} (${svcCustomers.from})`);
|
|
1644
|
-
info(`this command's own process ${where} — it does not load ${serviceEnvFile}, so that differs by `
|
|
1645
|
-
+ `design and is not what \`${invoke("start")}\` hands the services`);
|
|
1646
1631
|
} else {
|
|
1647
1632
|
if (r.situation === "overlay" && !r.findings.length) ok(`${where} — the configured store`);
|
|
1648
1633
|
else if (r.situation === "bundled-fallback") info(`${where} — THE BUNDLED DEMO ROSTER, because CLEAROTRON_CUSTOMERS_DIR is unset. Legitimate on a generic-defaults install; a fallback either way, and it is what a misconfigured deployment also looks like`);
|
|
@@ -1653,8 +1638,7 @@ export async function runCheck() {
|
|
|
1653
1638
|
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1654
1639
|
+ "what the services resolve is not judged here, and the line above is this process's own answer");
|
|
1655
1640
|
info("what a RUN used is its own `profile-store` journal line — this command reports what the "
|
|
1656
|
-
+ (hosted ? "units' file says, which the running services read at their own start"
|
|
1657
|
-
: `environment you are typing in and your environment file say, which \`${invoke("start")}\` reads at its own start`));
|
|
1641
|
+
+ (hosted ? "units' file says, which the running services read at their own start" : "environment you are typing in says"));
|
|
1658
1642
|
// ── AND WHO IS ACTUALLY IN IT ─────────────────────────────────────────
|
|
1659
1643
|
//
|
|
1660
1644
|
// The line above names the STORE. An operator who has just configured one wants to know their
|
|
@@ -1815,57 +1799,6 @@ export async function runCheck() {
|
|
|
1815
1799
|
}
|
|
1816
1800
|
}
|
|
1817
1801
|
|
|
1818
|
-
// — SAVED SEARCHES, JUDGED BY THE RULE THE PORTAL APPLIES WHEN IT STARTS. The portal switches them off,
|
|
1819
|
-
// and answers every saved-search route "not found", when the store is unset or sits outside the
|
|
1820
|
-
// repository its saves are committed in. Its boot log said so and nothing an operator runs did, so an
|
|
1821
|
-
// install whose Custom searches could never load passed this command. Same resolver, same containment
|
|
1822
|
-
// check, read from the services' environment for the reason the roster lines give; when that could not
|
|
1823
|
-
// be read, the lines above already said so and nothing is judged here. A store that is configured but
|
|
1824
|
-
// holds a file that cannot be read fails every company's saved searches, in the portal and the
|
|
1825
|
-
// connector alike, so that is read here too.
|
|
1826
|
-
//
|
|
1827
|
-
// WITH NO UNITS, THE SERVICES ARE `clearotron start`'s CHILDREN, and it hands every one of them a store
|
|
1828
|
-
// whether or not the env file names it. So "off" is only ever true of a hosted box. This read the env
|
|
1829
|
-
// file alone, and on a local install started before `start` wrote the store there, it told a portal
|
|
1830
|
-
// that was listing saved searches that they were off.
|
|
1831
|
-
if (!hosted || serviceKnown) {
|
|
1832
|
-
// Layered as effectiveForService layers it — this command's environment over the services' file — so
|
|
1833
|
-
// the repository root is read from the same place the store directory is.
|
|
1834
|
-
let storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
|
|
1835
|
-
let recipesSet = effectiveForService("CLEAROTRON_RECIPES_DIR");
|
|
1836
|
-
let handedBy = "";
|
|
1837
|
-
if (!hosted && !recipesSet?.v) {
|
|
1838
|
-
const handed = startPaths({ env: storeEnv });
|
|
1839
|
-
recipesSet = { v: handed.recipes, from: "clearotron start", name: "CLEAROTRON_RECIPES_DIR" };
|
|
1840
|
-
storeEnv = { ...storeEnv, RECIPE_REPO_ROOT: handed.configStore };
|
|
1841
|
-
handedBy = ` — where \`${invoke("start")}\` puts them`;
|
|
1842
|
-
}
|
|
1843
|
-
const recipesDir = recipesSet?.v || null;
|
|
1844
|
-
if (!recipesDir) {
|
|
1845
|
-
info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
|
|
1846
|
-
+ "connector offer none. Name a directory inside a git repository to switch them on");
|
|
1847
|
-
} else if (handedBy && !existsSync(join(storeEnv.RECIPE_REPO_ROOT, ".git"))) {
|
|
1848
|
-
// NOT CREATED YET IS NOT UNREADABLE. The store does not exist until the first start makes it, and
|
|
1849
|
-
// reading it now would report a missing directory as a broken one.
|
|
1850
|
-
info(`saved searches switch on at the first \`${invoke("start")}\`, which creates their store in ${recipesDir}`);
|
|
1851
|
-
} else {
|
|
1852
|
-
const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
|
|
1853
|
-
const reach = storeInRepo(recipesDir, resolved.root);
|
|
1854
|
-
if (!reach.ok) {
|
|
1855
|
-
warn(`saved searches are OFF: ${storeOutsideRepoMessage({ storeVar: "CLEAROTRON_RECIPES_DIR", storeDir: reach.store, repoVar: "RECIPE_REPO_ROOT", repoRoot: reach.repo })} `
|
|
1856
|
-
+ `The repository came from ${resolved.from}. Until this is fixed the portal answers every saved-search request as not found`);
|
|
1857
|
-
} else {
|
|
1858
|
-
const { loadRecipes } = await import("../driver/search-policy.mjs");
|
|
1859
|
-
let unreadable = null;
|
|
1860
|
-
try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
|
|
1861
|
-
if (unreadable) {
|
|
1862
|
-
warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
|
|
1863
|
-
+ "to load, in the portal and the connector, until it is fixed");
|
|
1864
|
-
} else ok(`saved searches are read from ${recipesDir}${handedBy}, and saves are committed in ${reach.repo}`);
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
1802
|
// — WHICH DOCTRINE FILES THIS INSTALL OVERRIDES, AND WHETHER OURS HAVE MOVED UNDER THEM.
|
|
1870
1803
|
//
|
|
1871
1804
|
// The report itself shipped in and worked, reachable only as `npm run doctrine-report` — a name
|
|
@@ -1904,8 +1837,7 @@ export async function runCheck() {
|
|
|
1904
1837
|
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1905
1838
|
+ "whether the services read an overlay is not judged here");
|
|
1906
1839
|
}
|
|
1907
|
-
if (report.ok && report.overlayConfigured)
|
|
1908
|
-
say(`\n Full detail: ${PACKAGED ? `node ${join(REPO, "scripts", "doctrine-report.mjs")}` : "npm run doctrine-report"}`);
|
|
1840
|
+
if (report.ok && report.overlayConfigured) say("\n Full detail: npm run doctrine-report");
|
|
1909
1841
|
} catch (e) {
|
|
1910
1842
|
// An unreadable overlay THROWS by design (config.resolveSkillPath refuses rather than falling back
|
|
1911
1843
|
// to the product's copy). Surfaced here rather than allowed to abort the whole check: the doctor's
|
|
@@ -1914,21 +1846,7 @@ export async function runCheck() {
|
|
|
1914
1846
|
}
|
|
1915
1847
|
|
|
1916
1848
|
say("\n Register provider");
|
|
1917
|
-
|
|
1918
|
-
// shell and this command's .env, so in a fresh terminal on a hosted box it told a working install that
|
|
1919
|
-
// no register was selected, one screen above "nothing a search is refused for is missing from the units'
|
|
1920
|
-
// environment": one install, two answers, and the fix it named was already set. effectiveForService is
|
|
1921
|
-
// the reader that check asks, so the two lines cannot disagree; where it cannot read the units, this
|
|
1922
|
-
// says it could not look rather than reporting the register absent.
|
|
1923
|
-
// Only a HOSTED box has a second environment to read; with no units, the services are started from
|
|
1924
|
-
// this command's own file, and `effective` has always read and labelled exactly that.
|
|
1925
|
-
const serviceValue = hosted ? effectiveForService : effective;
|
|
1926
|
-
const prov = serviceKnown ? serviceValue("CLEAROTRON_DATABASE") : null;
|
|
1927
|
-
if (!serviceKnown) {
|
|
1928
|
-
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1929
|
-
+ "which register the services search, and whether its keys are set, is not judged here: a failure to look, not a finding");
|
|
1930
|
-
}
|
|
1931
|
-
else
|
|
1849
|
+
const prov = effective("CLEAROTRON_DATABASE");
|
|
1932
1850
|
// `blocking`, not `warn` and not `problem`. The exit status is a CONTRACT — an absence reports and
|
|
1933
1851
|
// exits 0, a misconfiguration exits 1, and onboard-wizard.test.mjs holds it — so this cannot become a
|
|
1934
1852
|
// `problem` however much it stops the reader: an install that has not chosen a register yet is
|
|
@@ -1947,7 +1865,7 @@ export async function runCheck() {
|
|
|
1947
1865
|
else {
|
|
1948
1866
|
ok(`${spec.id} — ${spec.label} (${prov.from})`);
|
|
1949
1867
|
for (const k of spec.credentials) {
|
|
1950
|
-
const c =
|
|
1868
|
+
const c = effective(k);
|
|
1951
1869
|
// issue 1871 — SET, not WORKING, and the line now says which. An operator reads a tick as "this
|
|
1952
1870
|
// works"; this one is equally true of a valid key, an expired key, a key scoped to the wrong
|
|
1953
1871
|
// account and forty characters of nonsense. --probe-providers is what settles it.
|
|
@@ -1958,7 +1876,7 @@ export async function runCheck() {
|
|
|
1958
1876
|
// and never as a problem, but never silently either: the reader has to know which offices this
|
|
1959
1877
|
// box will not reach before they read a report that says nothing was found there.
|
|
1960
1878
|
for (const k of spec.optionalCredentials ?? []) {
|
|
1961
|
-
const c =
|
|
1879
|
+
const c = effective(k);
|
|
1962
1880
|
if (c) ok(`${k} present (${c.from}) — presence only; add --probe-providers to prove it retrieves`);
|
|
1963
1881
|
else info(`${k} is NOT set — ${spec.id} will run without it and DISCLOSE the offices it cannot reach as deferred coverage. Set it to search them.`);
|
|
1964
1882
|
}
|
|
@@ -1966,10 +1884,8 @@ export async function runCheck() {
|
|
|
1966
1884
|
}
|
|
1967
1885
|
|
|
1968
1886
|
say("\n Research provider");
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (!serviceKnown) info("the units' environment could not be read, so whether the services hold PERPLEXITY_API_KEY is not judged here — a failure to look, not a finding");
|
|
1972
|
-
else if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
|
|
1887
|
+
const px = effective("PERPLEXITY_API_KEY");
|
|
1888
|
+
if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
|
|
1973
1889
|
else info("PERPLEXITY_API_KEY is not set — the three clearance searches carry the common-law grid and cannot switch it off, so a clearance stops before it starts, and names the missing key; a Knockout search still runs and discloses the half it skipped");
|
|
1974
1890
|
|
|
1975
1891
|
// ── — THE LANES A PRODUCT DECLARES IT NEEDS, BEFORE A REPORT NAMES THEM ──
|
|
@@ -2238,26 +2154,15 @@ export async function runCheck() {
|
|
|
2238
2154
|
say("\n Portal sign-in");
|
|
2239
2155
|
{
|
|
2240
2156
|
const { authView } = await import("../driver/portal-config-view.mjs");
|
|
2241
|
-
// A BOX WITH NO UNITS RUNS THE PORTAL `clearotron start` LAUNCHES, as the line below says, and start
|
|
2242
|
-
// hands that portal `PORTAL_AUTH_MODE=local` and refuses any other declared mode (bin/start.mjs). So on
|
|
2243
|
-
// such a box the door is start's, not the bare service's fronted default. Reading the bare default told
|
|
2244
|
-
// a fresh home that the portal would refuse to start and that nobody could use it; `start` then came
|
|
2245
|
-
// up on the local sign-in and created the grants file (measured on a published beta, 2026-09-11).
|
|
2246
|
-
const startLaunched = !hosted;
|
|
2247
|
-
const declaredAuth = String(effectiveForService("PORTAL_AUTH_MODE")?.v ?? "").trim();
|
|
2248
2157
|
const door = authView({
|
|
2249
|
-
mode:
|
|
2158
|
+
mode: effectiveForService("PORTAL_AUTH_MODE")?.v ?? "",
|
|
2250
2159
|
oidcIssuer: effectiveForService("PORTAL_OIDC_ISSUER")?.v ?? "",
|
|
2251
2160
|
team: effectiveForService("CF_ACCESS_TEAM")?.v ?? "",
|
|
2252
2161
|
jwksUrl: effectiveForService("PORTAL_JWKS_URL")?.v ?? "",
|
|
2253
2162
|
emailClaim: effectiveForService("PORTAL_EMAIL_CLAIM")?.v ?? "",
|
|
2254
2163
|
authHeader: effectiveForService("PORTAL_AUTH_HEADER")?.v ?? "",
|
|
2255
2164
|
});
|
|
2256
|
-
const typed =
|
|
2257
|
-
: startLaunched ? "PORTAL_AUTH_MODE is unset, and `start` runs the local sign-in" : "PORTAL_AUTH_MODE is unset";
|
|
2258
|
-
if (startLaunched && declaredAuth && declaredAuth.toLowerCase() !== "local")
|
|
2259
|
-
problem(`${typed}: \`${reachableCommand("start")}\` refuses it, because start is the local install `
|
|
2260
|
-
+ "and runs the local sign-in; that mode belongs to a hosted deployment's units");
|
|
2165
|
+
const typed = door.declared ? `PORTAL_AUTH_MODE=${door.declared}` : "PORTAL_AUTH_MODE is unset";
|
|
2261
2166
|
// NOT A TICK, AND THAT IS THE POINT. This reads the environment THIS command is
|
|
2262
2167
|
// typed in. `bin/start.mjs` INJECTS `PORTAL_AUTH_MODE: "local"` into the portal's own environment,
|
|
2263
2168
|
// and a systemd unit's EnvironmentFile can name a third thing — so a green tick here was a
|
|
@@ -2322,13 +2227,7 @@ export async function runCheck() {
|
|
|
2322
2227
|
const { makePrincipal } = await import("../driver/portal-access.mjs");
|
|
2323
2228
|
// ASKED OF THE SERVICE'S OWN ENVIRONMENT. Reading this command's file here is what produced a hard
|
|
2324
2229
|
// ✗ claiming nobody could use a portal that was admitting its operator on every request.
|
|
2325
|
-
|
|
2326
|
-
// the person who started it (`paths.grants` in bin/start.mjs). Before that run it does not exist,
|
|
2327
|
-
// which is a first start still to come, not a portal nobody can use.
|
|
2328
|
-
const namedGrants = effectiveForService("CLEAROTRON_ACCESS_FILE")?.v ?? "";
|
|
2329
|
-
const startGrants = startLaunched && !namedGrants ? startPaths({ env: {} }).grants : null;
|
|
2330
|
-
const beforeFirstStart = Boolean(startGrants) && !existsSync(startGrants);
|
|
2331
|
-
const grantsFile = namedGrants || (beforeFirstStart ? "" : startGrants ?? "");
|
|
2230
|
+
const grantsFile = effectiveForService("CLEAROTRON_ACCESS_FILE")?.v ?? "";
|
|
2332
2231
|
let grants = null, unreadable = null;
|
|
2333
2232
|
if (grantsFile) {
|
|
2334
2233
|
try { grants = JSON.parse(readFileSync(grantsFile, "utf8")); }
|
|
@@ -2341,10 +2240,7 @@ export async function runCheck() {
|
|
|
2341
2240
|
// A FAILURE TO LOOK IS NOT A LOCKOUT. On a hosted box whose unit environment
|
|
2342
2241
|
// could not be read, every name above resolves empty — which is indistinguishable from a box that
|
|
2343
2242
|
// has genuinely configured nothing, and would print the loudest ✗ in this command on no evidence.
|
|
2344
|
-
if (
|
|
2345
|
-
info(`no grants file yet: the first \`${reachableCommand("start")}\` creates ${startGrants} and gives the person `
|
|
2346
|
-
+ "who runs it access to everything");
|
|
2347
|
-
} else if (!serviceKnown) {
|
|
2243
|
+
if (!serviceKnown) {
|
|
2348
2244
|
info("who may use this portal is not judged here: the units' environment could not be read, so a "
|
|
2349
2245
|
+ "grants file configured there would be invisible to this check");
|
|
2350
2246
|
} else if (unreadable) {
|
|
@@ -2954,39 +2850,6 @@ if (!input.isTTY) {
|
|
|
2954
2850
|
process.exit(2);
|
|
2955
2851
|
}
|
|
2956
2852
|
|
|
2957
|
-
// ── OUT OF NPX'S CACHE, BEFORE ANYTHING IS WRITTEN ─────────────────────────────────────────────────────
|
|
2958
|
-
//
|
|
2959
|
-
// Run from npx, this program lives in npm's cache, and everything below would be wired to a directory npm
|
|
2960
|
-
// deletes: the launcher, and the connect line an assistant is registered with. So the install first puts
|
|
2961
|
-
// this same version somewhere permanent (shared/permanent-install.mjs) and runs itself from there. Nothing
|
|
2962
|
-
// has been written yet, so a failure here costs nothing, and it stops rather than carrying on: an install
|
|
2963
|
-
// finished from the cache is the defect, not a fallback.
|
|
2964
|
-
const move = relocationPlan();
|
|
2965
|
-
if (move && !move.skip && process.env.CLEAROTRON_RELOCATED !== "1") {
|
|
2966
|
-
say(`\n This is running from npm's temporary npx cache. Installing clearotron ${move.version} to ${move.prefix}`);
|
|
2967
|
-
say(" first, so the launcher and your assistants keep working after npm cleans that cache or you update.\n");
|
|
2968
|
-
// The npm that launched this, when npm says which: no second npm is guessed at.
|
|
2969
|
-
const npmCli = process.env.npm_execpath;
|
|
2970
|
-
const r = npmCli && existsSync(npmCli)
|
|
2971
|
-
? spawnSync(process.execPath, [npmCli, ...move.npmArgs], { stdio: "inherit" })
|
|
2972
|
-
: spawnSync("npm", move.npmArgs, { stdio: "inherit" });
|
|
2973
|
-
if (r.status !== 0 || !existsSync(move.entry)) {
|
|
2974
|
-
console.error(`\n Could not install clearotron to ${move.prefix}${r.error ? ` (${r.error.message})` : ""}. Nothing was installed, and nothing of yours was changed.`);
|
|
2975
|
-
console.error(` Run \`npm install --global --prefix ${move.prefix} clearotron@${move.version}\`, then \`${join(move.prefix, "bin", "clearotron")} install\`.\n`);
|
|
2976
|
-
process.exit(1);
|
|
2977
|
-
}
|
|
2978
|
-
// THE REST OF THE INSTALL RUNS FROM THE PERMANENT COPY, with npm's marks of an npx arrival taken off, so
|
|
2979
|
-
// it prints the commands of the install it now is.
|
|
2980
|
-
const env = { ...process.env, CLEAROTRON_RELOCATED: "1" };
|
|
2981
|
-
for (const k of ["npm_command", "npm_lifecycle_event", "npm_execpath"]) delete env[k];
|
|
2982
|
-
const moved = spawnSync(process.execPath, [move.entry, "install", ...process.argv.slice(2)], { stdio: "inherit", env });
|
|
2983
|
-
process.exit(moved.status ?? 1);
|
|
2984
|
-
}
|
|
2985
|
-
if (move?.skip) {
|
|
2986
|
-
console.error(`\n Note: this is running from npm's temporary npx cache and cannot be moved out of it here (${move.skip}).`);
|
|
2987
|
-
console.error(" It will stop working when npm cleans that cache. `npm install -g clearotron` installs it permanently.\n");
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
2853
|
// A credential typed at a prompt is echoed by the terminal and then sits in scrollback, in tmux history,
|
|
2991
2854
|
// in whatever the reader pastes into a bug report. So the echo is muted while a secret is being typed:
|
|
2992
2855
|
// the output stream readline writes through drops everything while `muted` is set.
|
|
@@ -3367,7 +3230,7 @@ try {
|
|
|
3367
3230
|
// skipping it costs an hour later.
|
|
3368
3231
|
info("A file that exists is not an engine that works — so setup tries one before writing anything.");
|
|
3369
3232
|
info("It takes a few seconds here. Skipped, a broken engine surfaces an hour into a real search.");
|
|
3370
|
-
if (!await confirm(
|
|
3233
|
+
if (!await confirm(`Prove ${pick.id} on the ${authPick.id} lane now with one ${PROBE_MODEL}-tier turn (a few tokens, ${PROBE_TIMEOUT_SEC}s ceiling)?`, true)) {
|
|
3371
3234
|
info("Not proven, so not written. Pick again — the last row configures no engine at all.");
|
|
3372
3235
|
continue;
|
|
3373
3236
|
}
|
|
@@ -3781,14 +3644,6 @@ try {
|
|
|
3781
3644
|
candidate["CLEAROTRON_CUSTOMERS_DIR"] = join(cfg, "profiles");
|
|
3782
3645
|
candidate.PROFILE_REPO_ROOT = cfg; // no alias row — this name is current
|
|
3783
3646
|
for (const k of ["CLEAROTRON_CUSTOMERS_DIR", "PROFILE_REPO_ROOT"]) ok(`${k}=${candidate[k]}`);
|
|
3784
|
-
// A REPOSITORY ALREADY THERE IS ASKED NOW, while the operator is still here, whether it can record a
|
|
3785
|
-
// save. `clearotron start` gives a store it creates an identity of its own; one made by hand has none
|
|
3786
|
-
// unless somebody set it, and on a machine with no global identity the first company created in the
|
|
3787
|
-
// portal is refused. Said here, with the command, rather than discovered on that first company.
|
|
3788
|
-
if (existsSync(join(cfgAbs, ".git"))) {
|
|
3789
|
-
const cannot = storeCommitRefusal(cfgAbs);
|
|
3790
|
-
if (cannot) warn(`${cannot.message}. Until then, creating a company in the portal is refused.`);
|
|
3791
|
-
}
|
|
3792
3647
|
|
|
3793
3648
|
// CLEAROTRON_INSTRUCTIONS_DIR IS DELIBERATELY NOT WRITTEN (found in review).
|
|
3794
3649
|
//
|
|
@@ -3979,13 +3834,11 @@ try {
|
|
|
3979
3834
|
|
|
3980
3835
|
say(`\n ${style.bold("Start here:")}\n`);
|
|
3981
3836
|
say(` ${invocationPrefix()}clearotron start\n`);
|
|
3982
|
-
say(" Starts the portal and the engine door
|
|
3983
|
-
say("
|
|
3837
|
+
say(" Starts the portal and the engine door, prints one address, and opens it. That address is");
|
|
3838
|
+
say(" the product: you order a clearance from it and read the report there.\n");
|
|
3984
3839
|
say(` ${style.dim(`Also: \`${invocationPrefix()}clearotron demo\` replays a finished report with no keys and no model calls;`)}`);
|
|
3985
|
-
say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job
|
|
3986
|
-
|
|
3987
|
-
if (!PACKAGED) say(` ${style.dim("Each still works the old way too — `npm start`, `npm run example`, `node driver/pipeline.mjs`.")}`);
|
|
3988
|
-
say("");
|
|
3840
|
+
say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job examples/job.euipo.json\` runs a first real clearance on the EU register.`)}`);
|
|
3841
|
+
say(` ${style.dim("Each still works the old way too — `npm start`, `npm run example`, `node driver/pipeline.mjs`.")}\n`);
|
|
3989
3842
|
|
|
3990
3843
|
// WHY THOSE LINES LOOK THE WAY THEY DO, when they are not the bare verb.
|
|
3991
3844
|
//
|