quoxcli 0.2.0 → 0.3.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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -15,18 +15,31 @@ export const KEY_PROVIDERS = new Set(['openai', 'anthropic']);
|
|
|
15
15
|
export const CHATGPT_ENABLED = false;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Options in display order.
|
|
19
|
-
*
|
|
18
|
+
* Options in display order. #1288: the choices that work come first. Sign in with ChatGPT is not built
|
|
19
|
+
* yet (waiting on OpenAI), so while it is off it is listed LAST as "coming soon", never first; once it
|
|
20
|
+
* is on it leads (OpenAI-first). Ollama is always offered (B3): "on this machine" first when
|
|
21
|
+
* localhost:11434 answered, else "on another machine" (asks for its LAN URL) before Skip.
|
|
20
22
|
*/
|
|
21
23
|
export function credentialOptions({ ollama = false, chatgpt = CHATGPT_ENABLED } = {}) {
|
|
22
|
-
|
|
23
|
-
const order =
|
|
24
|
+
const working = ollama ? ['ollama', 'openai', 'anthropic', 'skip'] : ['openai', 'anthropic', 'ollama', 'skip'];
|
|
25
|
+
const order = chatgpt ? ['chatgpt', ...working] : [...working, 'chatgpt'];
|
|
24
26
|
return order.map(id => {
|
|
25
27
|
if (id === 'ollama') return { id, ...(ollama ? CRED_OPTIONS.ollama : CRED_OPTIONS.ollamaLan), detected: Boolean(ollama), disabled: false };
|
|
26
28
|
return { id, ...CRED_OPTIONS[id], disabled: id === 'chatgpt' && !chatgpt };
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** The next selectable row from `sel` in direction dir (1 down, -1 up), wrapping; disabled rows are skipped. */
|
|
33
|
+
export function nextSelectable(options, sel, dir) {
|
|
34
|
+
const n = options.length;
|
|
35
|
+
if (!n) return 0;
|
|
36
|
+
for (let k = 1; k <= n; k++) {
|
|
37
|
+
const i = (((sel + dir * k) % n) + n) % n;
|
|
38
|
+
if (!options[i].disabled) return i;
|
|
39
|
+
}
|
|
40
|
+
return sel;
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
/** A typed Ollama URL -> normalised http(s) URL (bare host gets http:// and :11434) | null. */
|
|
31
44
|
export function normaliseOllamaUrl(text) {
|
|
32
45
|
let v = String(text || '').trim();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIRSTRUN (2026-10-19): the guide OFFERS to fix Docker instead of saying "install Docker, then press r".
|
|
3
|
+
*
|
|
4
|
+
* install Docker missing (Linux): Docker's official script, then add this user to the docker group
|
|
5
|
+
* group Docker installed, this user not in the docker group: add them
|
|
6
|
+
* relaunch in the group, but this terminal started before that: nothing to run with sudo
|
|
7
|
+
*
|
|
8
|
+
* A yes leaves the Ink view (the journey is saved at preflight), runs the command with the real terminal
|
|
9
|
+
* attached so sudo can ask for a password, then starts quox again under `sg docker`, so the new group applies
|
|
10
|
+
* without logging out. The relaunched quox picks up where the guide left off.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { askText } from '../ask.js';
|
|
14
|
+
import * as T from './copy.js';
|
|
15
|
+
|
|
16
|
+
export const DOCKER_INSTALL_CMD = 'curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker "$USER"';
|
|
17
|
+
export const DOCKER_GROUP_CMD = 'sudo usermod -aG docker "$USER"';
|
|
18
|
+
const COMMANDS = { install: DOCKER_INSTALL_CMD, group: DOCKER_GROUP_CMD, relaunch: null };
|
|
19
|
+
|
|
20
|
+
/** rows -> { kind, command, ask } for the first row the guide can fix, or null. */
|
|
21
|
+
export function dockerOffer(rows) {
|
|
22
|
+
const r = (rows || []).find(x => x?.status === 'fail' && x.offer && x.offer in COMMANDS);
|
|
23
|
+
if (!r) return null;
|
|
24
|
+
const kind = r.offer;
|
|
25
|
+
const command = COMMANDS[kind];
|
|
26
|
+
const q = T.DOCKER_OFFER_QUESTION[kind];
|
|
27
|
+
const action = command ? `run: ${command} then start quox again with the docker group` : 'start quox again with the docker group (sg docker), no sudo';
|
|
28
|
+
return { kind, command, ask: askText(q, { keys: 'y/N', who: 'Quox', action }) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const shq = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
32
|
+
|
|
33
|
+
/** Runs the offer with the terminal attached. -> { action: 'exit', status } */
|
|
34
|
+
export function runDockerOffer(kind, { spawn = spawnSync, out = (s) => process.stdout.write(s), node = process.execPath, bin } = {}) {
|
|
35
|
+
const command = COMMANDS[kind];
|
|
36
|
+
if (command) {
|
|
37
|
+
out(`\n${T.DOCKER_OFFER_RUNNING}\n ${command}\n\n`);
|
|
38
|
+
const r = spawn('sh', ['-c', command], { stdio: 'inherit' });
|
|
39
|
+
if (r?.status !== 0) { out(`\n${T.DOCKER_OFFER_FAILED}\n`); return { action: 'exit', status: r?.status ?? 1 }; }
|
|
40
|
+
}
|
|
41
|
+
out(`\n${T.DOCKER_OFFER_RELAUNCH}\n`);
|
|
42
|
+
const again = spawn('sg', ['docker', '-c', `exec ${shq(node)} ${shq(bin)}`], { stdio: 'inherit' });
|
|
43
|
+
if (again?.error) { out(`${T.CHECK.docker.permStaleFix}\n`); return { action: 'exit', status: 1 }; }
|
|
44
|
+
return { action: 'exit', status: again?.status ?? 0 };
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FIRSTRUN (2026-10-19): a failed install says WHY in one plain line, for the causes a stranger can act on.
|
|
3
|
+
* Input: installer output plus the tail of the build log setup.sh names. Output: { kind, host?, message } | null.
|
|
4
|
+
* Prebuilt images being unavailable ("error from registry: denied") is never a cause: the installer builds instead.
|
|
5
|
+
*/
|
|
6
|
+
const RETRY = 'then press r to try again.';
|
|
7
|
+
|
|
8
|
+
const NETWORK = [
|
|
9
|
+
/Could not connect to ([a-z0-9.-]+\.[a-z]{2,}):\d+/i,
|
|
10
|
+
/Temporary failure resolving '([^']+)'/i,
|
|
11
|
+
/Failed to fetch https?:\/\/([a-z0-9.-]+\.[a-z]{2,})\/\S*\s+(?:Could not connect|Unable to connect|Connection failed|Temporary failure)/i,
|
|
12
|
+
/request to https?:\/\/([a-z0-9.-]+\.[a-z]{2,})\/\S* failed, reason: (?:connect )?(?:ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|ENOTFOUND)/i,
|
|
13
|
+
/getaddrinfo (?:EAI_AGAIN|ENOTFOUND) ([a-z0-9.-]+\.[a-z]{2,})/i
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function explainInstallFailure(text) {
|
|
17
|
+
const t = String(text ?? '');
|
|
18
|
+
if (!t.trim()) return null;
|
|
19
|
+
for (const re of NETWORK) {
|
|
20
|
+
const m = re.exec(t);
|
|
21
|
+
if (m) {
|
|
22
|
+
const host = m[1].toLowerCase();
|
|
23
|
+
return { kind: 'network', host, message: `The build could not reach ${host}, a package server. Check this machine's internet connection, firewall or proxy, ${RETRY}` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (/no space left on device/i.test(t)) {
|
|
27
|
+
return { kind: 'disk', message: `The disk filled up during the build. Free some space (docker system prune -f frees old build cache), ${RETRY}` };
|
|
28
|
+
}
|
|
29
|
+
if (/heap out of memory|exit code: 137\b|\bOOMKilled\b|Cannot allocate memory/i.test(t)) {
|
|
30
|
+
return { kind: 'memory', message: `The build ran out of memory. Close other programs or give this machine more RAM (8 GB is recommended), ${RETRY}` };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -107,6 +107,21 @@ export async function createFinisher({ profileUrl = LOCAL_PROFILE_URL, fetchImpl
|
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
|
|
110
|
+
/** QUOXON-Z2H lane R: the owner said yes at the credential step; record it as their own act. -> { ok } | { ok: false, error } */
|
|
111
|
+
async grantConsent(provider) {
|
|
112
|
+
try { await createApi({ client, config, auth }).grantConsent(provider); return { ok: true }; }
|
|
113
|
+
catch (e) { return { ok: false, error: apiError(e, 'consent not recorded') }; }
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/** Z2H-R5 E: can this server serve its host installer? -> { ok, status } | null (unknown). Reads only. */
|
|
117
|
+
async hostInstaller() {
|
|
118
|
+
try {
|
|
119
|
+
const api = createApi({ client, config, auth });
|
|
120
|
+
const base = await api.hostsBaseUrl();
|
|
121
|
+
return await api.hostInstallCheck(base);
|
|
122
|
+
} catch { return null; }
|
|
123
|
+
},
|
|
124
|
+
|
|
110
125
|
/** -> { ok: true, url, expiresAt, ttlSeconds } | { ok: false, error } */
|
|
111
126
|
async loginLink(dashboardUrl) {
|
|
112
127
|
try {
|
|
@@ -7,6 +7,10 @@ import React from 'react';
|
|
|
7
7
|
import { render } from 'ink';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { probeQuoxCore, probeMessage } from './probe.js';
|
|
10
14
|
import { GettingStarted } from './view.js';
|
|
11
15
|
import { inkStdout } from '../term.js';
|
|
12
16
|
import { createCredentialBroker, createRequestCredential } from './credential.js';
|
|
@@ -15,6 +19,7 @@ import { createInstallRunner, defaultInstallDir, probeAuthHealth } from './insta
|
|
|
15
19
|
import { createFinisher } from './finish.js';
|
|
16
20
|
import { journeyPath as defaultJourneyPath, clearJourney, loadJourney } from './journey.js';
|
|
17
21
|
import * as T from './copy.js';
|
|
22
|
+
import { runDockerOffer } from './dockerOffer.js';
|
|
18
23
|
|
|
19
24
|
const QUOX_BIN = fileURLToPath(new URL('../../../bin/quox.js', import.meta.url));
|
|
20
25
|
|
|
@@ -49,19 +54,41 @@ export async function runGettingStarted({ version, cwd, welcome = false, earlyIn
|
|
|
49
54
|
await app.waitUntilExit();
|
|
50
55
|
broker.cancel();
|
|
51
56
|
if (result.action === 'connect') return connectServer(result.url, { demo });
|
|
57
|
+
if (result.action === 'docker' && !demo) return runDockerOffer(result.kind, { bin: QUOX_BIN }); // FIRSTRUN
|
|
52
58
|
return result;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
61
|
+
/**
|
|
62
|
+
* /connect server <url>: check the URL is a real QuoxCORE (#1101), point the active profile at it and run the
|
|
63
|
+
* existing login. Any outcome but a completed sign-in (failure, Ctrl-C at Email or Password) puts the previous
|
|
64
|
+
* URL and the last-good config.json.bak back, so no half-saved profile is left behind.
|
|
65
|
+
*/
|
|
66
|
+
export async function connectServer(url, {
|
|
67
|
+
demo = false, spawn = spawnSync, out = (s) => process.stdout.write(s),
|
|
68
|
+
probe = probeQuoxCore, loadConfigImpl = null, bakFile = join(homedir(), '.config', 'quox', 'config.json.bak'),
|
|
69
|
+
localCertPath = join(defaultInstallDir(), 'quox', 'ssl', 'quox.crt') // the guided install's own certificate, if this is the server
|
|
70
|
+
} = {}) {
|
|
57
71
|
if (demo) { out(`${T.connectHandoff(url)}\n(demo: not connecting)\n`); return { action: 'exit' }; }
|
|
58
|
-
const
|
|
59
|
-
|
|
72
|
+
const p = await probe(url);
|
|
73
|
+
if (!p?.ok) { out(`${probeMessage(url, p, { localCert: existsSync(localCertPath) ? localCertPath : null })}\n`); return { action: 'connect-failed' }; }
|
|
74
|
+
const load = loadConfigImpl || (await import('../../core/config.js')).loadConfig;
|
|
75
|
+
const config = load();
|
|
60
76
|
const prev = config.getActiveProfile()?.url || null;
|
|
77
|
+
let bak = null;
|
|
78
|
+
try { bak = readFileSync(bakFile); } catch { /* no backup yet */ }
|
|
61
79
|
config.setProfileConnection({ url });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
out(`${T.connectHandoff(url)}\n`);
|
|
81
|
+
// Ctrl-C reaches the whole foreground group: the login child handles it, the parent must live to restore.
|
|
82
|
+
const hold = () => {};
|
|
83
|
+
process.on('SIGINT', hold);
|
|
84
|
+
let r;
|
|
85
|
+
try { r = spawn(process.execPath, [QUOX_BIN, 'login'], { stdio: 'inherit' }); } finally { process.removeListener('SIGINT', hold); }
|
|
86
|
+
if (r?.status === 0) { clearJourney(); out(`${T.CONNECT_DONE}\n`); return { action: 'connected' }; }
|
|
87
|
+
const cfg = load();
|
|
88
|
+
if (prev) cfg.setProfileConnection({ url: prev });
|
|
89
|
+
else { const prof = cfg.getActiveProfile(); if (prof) { delete prof.url; cfg.save(); } }
|
|
90
|
+
if (bak) { try { writeFileSync(bakFile, bak, { mode: 0o600 }); } catch { /* best effort */ } }
|
|
91
|
+
if (r?.status === 130 || r?.signal === 'SIGINT') { out(`\x1b[?25h${T.CONNECT_CANCELLED}\n`); return { action: 'connect-cancelled' }; }
|
|
65
92
|
out(`${T.connectFailed(url)}\n`);
|
|
66
93
|
return { action: 'connect-failed' };
|
|
67
94
|
}
|
|
@@ -22,13 +22,17 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
25
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, appendFileSync, openSync, readSync, fstatSync, closeSync } from 'node:fs';
|
|
26
|
+
import { explainInstallFailure } from './failureCause.js';
|
|
26
27
|
import { homedir, tmpdir } from 'node:os';
|
|
27
28
|
import { join } from 'node:path';
|
|
28
29
|
import { STAGE_IDS, stageList } from './runner.js';
|
|
29
30
|
|
|
31
|
+
import { BUILD_LABELS } from './copy.js';
|
|
30
32
|
export const INSTALLER_URL = 'https://get.quox.ai';
|
|
31
33
|
export const defaultInstallDir = (home = homedir()) => join(home, 'quox-install');
|
|
34
|
+
/** Where the full install log lives: the user's quox state dir, ~/.local/state/quox/install-logs. */
|
|
35
|
+
export const defaultInstallLogDir = (home = homedir()) => join(home, '.local', 'state', 'quox', 'install-logs');
|
|
32
36
|
|
|
33
37
|
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
34
38
|
const RED = /^\x1b\[(0;)?31m/;
|
|
@@ -44,7 +48,7 @@ const MARKS = [
|
|
|
44
48
|
* push(rawLine) -> { line: cleaned text | null, moves: [[stageId, 'ok'|'running']] }
|
|
45
49
|
*/
|
|
46
50
|
export function createStageParser() {
|
|
47
|
-
const state = { current: 'fetch', ready: false, degraded: false, dashboardUrl: null, lastError: null };
|
|
51
|
+
const state = { current: 'fetch', buildMode: null, ready: false, degraded: false, dashboardUrl: null, lastError: null };
|
|
48
52
|
return {
|
|
49
53
|
state,
|
|
50
54
|
push(raw, { stderr = false } = {}) {
|
|
@@ -61,6 +65,9 @@ export function createStageParser() {
|
|
|
61
65
|
state.current = next;
|
|
62
66
|
break;
|
|
63
67
|
}
|
|
68
|
+
// Z2H-IMAGES: setup.sh either downloads prebuilt images or builds them; the build stage says which.
|
|
69
|
+
if (/Downloading prebuilt images/.test(line)) state.buildMode = 'download';
|
|
70
|
+
else if (/Building images/.test(line)) state.buildMode = 'build';
|
|
64
71
|
if (/QuoxCORE full platform is ready/.test(line)) state.ready = true;
|
|
65
72
|
if (/core shell is up, but platform features are degraded/.test(line)) state.degraded = true;
|
|
66
73
|
const open = /^\s*Open:\s+(https?:\/\/\S+)/.exec(line);
|
|
@@ -88,6 +95,7 @@ export function createInstallRunner({
|
|
|
88
95
|
env = process.env,
|
|
89
96
|
url = env.QUOX_INSTALLER_URL || INSTALLER_URL,
|
|
90
97
|
installDir = defaultInstallDir(),
|
|
98
|
+
logDir = defaultInstallLogDir(),
|
|
91
99
|
fetchImpl = globalThis.fetch,
|
|
92
100
|
spawnImpl = defaultSpawn,
|
|
93
101
|
tmpRoot = tmpdir(),
|
|
@@ -118,7 +126,44 @@ export function createInstallRunner({
|
|
|
118
126
|
installDir,
|
|
119
127
|
stages: stageList(),
|
|
120
128
|
/** Per-run overrides (the view passes the journey's checkpoint): resumeFrom, resumeDashboardUrl, onCheckpoint. */
|
|
121
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Every run also writes the full stage log (each line the installer printed) to a file under the
|
|
131
|
+
* user's quox state dir; the result carries `logPath` so the UI can show it on failure and at the end.
|
|
132
|
+
*/
|
|
133
|
+
async run(opts = {}) {
|
|
134
|
+
const { onEvent = () => {} } = opts;
|
|
135
|
+
let logPath = null;
|
|
136
|
+
try {
|
|
137
|
+
mkdirSync(logDir, { recursive: true, mode: 0o700 });
|
|
138
|
+
logPath = join(logDir, `install-${new Date(now()).toISOString().replace(/[:.]/g, '-')}.log`);
|
|
139
|
+
writeFileSync(logPath, `# quox install log, started ${new Date(now()).toISOString()}\n`, { mode: 0o600 });
|
|
140
|
+
} catch { logPath = null; } // best effort: a log that cannot be written never fails the install
|
|
141
|
+
const recent = []; // FIRSTRUN: the last lines, so a failure can say its cause
|
|
142
|
+
const tee = (e) => {
|
|
143
|
+
if (e.type === 'log') { recent.push(e.line); if (recent.length > 400) recent.shift(); }
|
|
144
|
+
if (logPath) {
|
|
145
|
+
try {
|
|
146
|
+
if (e.type === 'log') appendFileSync(logPath, e.line + '\n');
|
|
147
|
+
else if (e.type === 'stage') appendFileSync(logPath, `-- stage ${e.id} ${e.status}\n`);
|
|
148
|
+
} catch { /* best effort */ }
|
|
149
|
+
}
|
|
150
|
+
onEvent(e);
|
|
151
|
+
};
|
|
152
|
+
const res = await runCore({ ...opts, onEvent: tee });
|
|
153
|
+
if (!res.ok && !res.interrupted) {
|
|
154
|
+
const named = recent.map(l => /Full build log:\s*(\S+)/.exec(l)?.[1]).filter(Boolean).pop();
|
|
155
|
+
const cause = explainInstallFailure(`${recent.join('\n')}\n${res.error || ''}\n${named ? tailOf(named) : ''}`);
|
|
156
|
+
if (cause) res.cause = cause;
|
|
157
|
+
}
|
|
158
|
+
if (!res.ok && !res.interrupted && logPath) {
|
|
159
|
+
// Z2H-R2: a failed run always says where the full log is and what to do next.
|
|
160
|
+
res.hint = `Full log: ${logPath}. It is safe to run the installer again; finished steps are kept and the build is cached.`;
|
|
161
|
+
}
|
|
162
|
+
if (logPath) { try { appendFileSync(logPath, `-- result ${res.ok ? 'ok' : `failed at ${res.failedStage || '?'}: ${res.error || ''}`}\n`); } catch { /* best effort */ } }
|
|
163
|
+
return logPath ? { ...res, logPath } : res;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
async function runCore({ onEvent = () => {}, signal, resumeFrom: rf = resumeFrom, resumeDashboardUrl: rd = resumeDashboardUrl, onCheckpoint: oc = onCheckpoint } = {}) {
|
|
122
167
|
const resumeAfterStart = rf === 'start' || rf === 'health';
|
|
123
168
|
const started = {};
|
|
124
169
|
const stage = (id, status) => {
|
|
@@ -185,11 +230,13 @@ export function createInstallRunner({
|
|
|
185
230
|
};
|
|
186
231
|
};
|
|
187
232
|
const take = (raw, stderr) => {
|
|
233
|
+
const modeBefore = parser.state.buildMode;
|
|
188
234
|
const { line, moves } = parser.push(raw, { stderr });
|
|
189
235
|
for (const [id, status] of moves) {
|
|
190
236
|
if (status === 'ok') { stage(id, 'ok'); if (id !== 'health') oc(id, {}); }
|
|
191
237
|
else stage(id, 'running');
|
|
192
238
|
}
|
|
239
|
+
if (parser.state.buildMode !== modeBefore) emit({ type: 'stage-label', id: 'build', label: BUILD_LABELS[parser.state.buildMode] });
|
|
193
240
|
if (line) emit({ type: 'log', line });
|
|
194
241
|
};
|
|
195
242
|
child.stdout.on('data', feed(false));
|
|
@@ -220,6 +267,18 @@ export function createInstallRunner({
|
|
|
220
267
|
} finally {
|
|
221
268
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* temp cleanup only */ }
|
|
222
269
|
}
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The last `max` bytes of a file, '' when it cannot be read (a cause is best effort, never a new failure). */
|
|
274
|
+
function tailOf(path, max = 256 * 1024) {
|
|
275
|
+
let fd = null;
|
|
276
|
+
try {
|
|
277
|
+
fd = openSync(path, 'r');
|
|
278
|
+
const size = fstatSync(fd).size;
|
|
279
|
+
const len = Math.min(size, max);
|
|
280
|
+
const buf = Buffer.alloc(len);
|
|
281
|
+
readSync(fd, buf, 0, len, size - len);
|
|
282
|
+
return buf.toString('utf8');
|
|
283
|
+
} catch { return ''; } finally { if (fd != null) try { closeSync(fd); } catch { /* closed */ } }
|
|
225
284
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { execFile } from 'node:child_process';
|
|
10
10
|
import { statfs } from 'node:fs/promises';
|
|
11
|
-
import { totalmem, homedir } from 'node:os';
|
|
11
|
+
import { totalmem, homedir, userInfo } from 'node:os';
|
|
12
12
|
import { createConnection } from 'node:net';
|
|
13
13
|
import { CHECK } from './copy.js';
|
|
14
14
|
|
|
@@ -55,13 +55,27 @@ export const defaultDeps = () => ({
|
|
|
55
55
|
const gb = (bytes) => Math.floor(bytes / 1024 ** 3);
|
|
56
56
|
const row = (id, status, body, extra = {}) => ({ id, name: CHECK[id].name, status, body, ...extra });
|
|
57
57
|
|
|
58
|
+
const safeUser = () => { try { return userInfo().username; } catch { return ''; } };
|
|
59
|
+
|
|
58
60
|
export async function checkDocker(d) {
|
|
59
61
|
const v = await d.run('docker', ['--version'], 5000);
|
|
60
|
-
|
|
62
|
+
const linux = (d.platform || process.platform) === 'linux'; // FIRSTRUN: the guide can fix these itself (dockerOffer.js)
|
|
63
|
+
const offer = (kind) => (linux ? { offer: kind } : {});
|
|
64
|
+
if (v.missing || v.code !== 0) return row('docker', 'fail', CHECK.docker.missing, { fix: CHECK.docker.missingFix, link: CHECK.docker.missingLink, ...offer('install') });
|
|
61
65
|
const ver = (/version\s+([0-9][\w.\-+]*)/i.exec(v.stdout) || [])[1] || '';
|
|
62
66
|
const info = await d.run('docker', ['info', '--format', '{{.ServerVersion}}'], 10000);
|
|
63
67
|
if (info.code === 0 && info.stdout.trim()) return row('docker', 'ok', CHECK.docker.ok(ver || info.stdout.trim()));
|
|
64
|
-
if (/permission denied/i.test(info.stderr))
|
|
68
|
+
if (/permission denied/i.test(info.stderr)) {
|
|
69
|
+
// Z2H-R5 D: the group was added after this terminal started. `id -nG` shows the groups of THIS process,
|
|
70
|
+
// `id -nG <user>` the account database; in the second but not the first, "press r" can never pass.
|
|
71
|
+
const user = d.user || safeUser();
|
|
72
|
+
if (user) {
|
|
73
|
+
const [mine, db] = await Promise.all([d.run('id', ['-nG'], 3000), d.run('id', ['-nG', user], 3000)]);
|
|
74
|
+
const has = (r) => r.code === 0 && r.stdout.trim().split(/\s+/).includes('docker');
|
|
75
|
+
if (has(db) && !has(mine)) return row('docker', 'fail', CHECK.docker.permStale, { fix: CHECK.docker.permStaleFix, link: CHECK.docker.link, stale: true, ...offer('relaunch') });
|
|
76
|
+
}
|
|
77
|
+
return row('docker', 'fail', CHECK.docker.perm, { fix: CHECK.docker.permFix, link: CHECK.docker.link, ...offer('group') });
|
|
78
|
+
}
|
|
65
79
|
return row('docker', 'fail', CHECK.docker.down, { fix: CHECK.docker.downFix, link: CHECK.docker.link });
|
|
66
80
|
}
|
|
67
81
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1101: is this URL a real QuoxCORE? One cheap unauthenticated GET of the edge's /health (nginx.conf answers
|
|
3
|
+
* {"status":"ok","service":"dashboard"} on both the HTTP and HTTPS servers), run BEFORE /connect server asks for
|
|
4
|
+
* an email and password. An error body is not data: only a 2xx JSON answer of that exact shape passes.
|
|
5
|
+
*
|
|
6
|
+
* -> { ok: true } | { ok: false, kind: 'url'|'unreachable'|'tls'|'not-quoxcore', code?, status?, detail? }
|
|
7
|
+
*/
|
|
8
|
+
const TLS_CODES = /^(SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE|UNABLE_TO_GET_ISSUER_CERT(_LOCALLY)?|CERT_HAS_EXPIRED|CERT_NOT_YET_VALID|ERR_TLS_.*|ERR_SSL_.*|EPROTO)$/;
|
|
9
|
+
const PLAINTEXT_CODES = new Set(['ERR_SSL_WRONG_VERSION_NUMBER', 'EPROTO']);
|
|
10
|
+
|
|
11
|
+
export async function probeQuoxCore(url, { fetchImpl = globalThis.fetch, timeout = 5000 } = {}) {
|
|
12
|
+
let base;
|
|
13
|
+
try {
|
|
14
|
+
base = new URL(String(url || ''));
|
|
15
|
+
if (base.protocol !== 'http:' && base.protocol !== 'https:') return { ok: false, kind: 'url', detail: 'the address must start with https:// or http://' };
|
|
16
|
+
} catch { return { ok: false, kind: 'url', detail: 'the address must start with https:// or http://' }; }
|
|
17
|
+
const target = `${base.href.replace(/\/+$/, '')}/health`;
|
|
18
|
+
let res;
|
|
19
|
+
try {
|
|
20
|
+
res = await fetchImpl(target, { signal: AbortSignal.timeout(timeout), headers: { accept: 'application/json' } });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
const code = e?.cause?.code || e?.code || null;
|
|
23
|
+
if (code && TLS_CODES.test(code)) return { ok: false, kind: 'tls', code };
|
|
24
|
+
if (e?.name === 'TimeoutError' || e?.name === 'AbortError') return { ok: false, kind: 'unreachable', code: 'TIMEOUT', detail: `no answer within ${Math.round(timeout / 1000)} s` };
|
|
25
|
+
return { ok: false, kind: 'unreachable', code, detail: e?.message || 'no answer' };
|
|
26
|
+
}
|
|
27
|
+
if (res?.status === 400 && base.protocol === 'http:') {
|
|
28
|
+
let t = ''; try { t = await res.text(); } catch { /* no body */ }
|
|
29
|
+
if (/plain HTTP request was sent to HTTPS port/i.test(t)) return { ok: false, kind: 'tls', code: 'HTTP_TO_HTTPS_PORT' };
|
|
30
|
+
}
|
|
31
|
+
if (!res || !(res.status >= 200 && res.status < 300)) return { ok: false, kind: 'not-quoxcore', status: res?.status, detail: `HTTP ${res?.status} from /health` };
|
|
32
|
+
let body;
|
|
33
|
+
try { body = JSON.parse(await res.text()); } catch { return { ok: false, kind: 'not-quoxcore', status: res.status, detail: '/health did not answer JSON' }; }
|
|
34
|
+
if (!body || body.status !== 'ok' || body.service !== 'dashboard') return { ok: false, kind: 'not-quoxcore', status: res.status, detail: '/health answered, but not as QuoxCORE does' };
|
|
35
|
+
return { ok: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A new QuoxCORE serves its own self-signed certificate (setup.sh writes ssl/quox.crt, CA:TRUE, SAN = localhost +
|
|
39
|
+
// the LAN IP). Trusting exactly that file is the sanctioned fix; these are the codes it fixes. An expired or
|
|
40
|
+
// wrong-name certificate is a different problem: trusting a file would not cure it, so it is not suggested.
|
|
41
|
+
const SELF_SIGNED_CODES = new Set(['DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY']);
|
|
42
|
+
export const SERVER_CERT_DEFAULT = '~/quox-install/quox/ssl/quox.crt';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One plain message per failure kind, naming the address and the next step.
|
|
46
|
+
* opts.localCert: the absolute path of a QuoxCORE certificate on THIS machine (the server is here), if any.
|
|
47
|
+
*/
|
|
48
|
+
export function probeMessage(url, r, { localCert = null } = {}) {
|
|
49
|
+
const again = 'Check the address, then try /connect server <url> again.';
|
|
50
|
+
switch (r?.kind) {
|
|
51
|
+
case 'url': return `"${url}" is not a server address (${r.detail}). Example: /connect server https://quox.example.com`;
|
|
52
|
+
case 'unreachable': return `No answer from ${url}${r.code ? ` (${r.code})` : ''}. Nothing was saved and no password was asked for. ${again}`;
|
|
53
|
+
case 'tls':
|
|
54
|
+
if (r.code === 'HTTP_TO_HTTPS_PORT') return `${url} is an HTTPS port spoken to in plain HTTP. Did you mean ${String(url).replace(/^http:/i, 'https:')} ? Nothing was saved.`;
|
|
55
|
+
if (PLAINTEXT_CODES.has(r.code)) return `${url} answered plain HTTP to an https:// request (${r.code}). Did you mean ${String(url).replace(/^https:/i, 'http:')} ? Nothing was saved.`;
|
|
56
|
+
if (SELF_SIGNED_CODES.has(r.code)) {
|
|
57
|
+
const lead = `${url} answered with its own self-signed certificate (${r.code}), which is normal for a new QuoxCORE. Trust that server's certificate, then start quox again.`;
|
|
58
|
+
const tail = 'Never turn TLS checks off. Nothing was saved.';
|
|
59
|
+
if (localCert) return `${lead} It is on this machine, so run: NODE_EXTRA_CA_CERTS=${localCert} quox ${tail}`;
|
|
60
|
+
return `${lead} Copy it from the server (by default ${SERVER_CERT_DEFAULT}) to this machine, then run: NODE_EXTRA_CA_CERTS=~/quox.crt quox ${tail}`;
|
|
61
|
+
}
|
|
62
|
+
return `${url} answered, but its TLS certificate is not trusted (${r.code}). For a self-signed edge, trust its CA with NODE_EXTRA_CA_CERTS=<ca.pem> and try again; never turn TLS checks off. Nothing was saved.`;
|
|
63
|
+
default: return `${url} answered, but it is not a QuoxCORE server (${r?.detail || 'unexpected answer'}). Nothing was saved and no password was asked for. ${again}`;
|
|
64
|
+
}
|
|
65
|
+
}
|