quoxcli 0.3.1 → 0.5.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.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox server install`: the existing guided installer behind a CLI verb (ONBOARD-P1).
|
|
3
|
+
*
|
|
4
|
+
* Not a second installer. Preflight (onboarding/preflight.js), the real runner (installRunner.js: get.quox.ai ->
|
|
5
|
+
* setup.sh) and the finisher (finish.js: owner account, sign-in, one-time link) are the same modules the Getting
|
|
6
|
+
* started screen uses; this file only sequences them and turns their events into stage events:
|
|
7
|
+
*
|
|
8
|
+
* preflight docker download compose-up health owner vault link
|
|
9
|
+
*
|
|
10
|
+
* The runner's own stages fold in as: fetch + configure + build -> download, start -> compose-up, health -> health.
|
|
11
|
+
* `vault` is skipped here (no AI key is taken on argv); the next step is printed.
|
|
12
|
+
* Nothing is claimed ok that did not answer: health is the runner's own 2xx probe of the auth service.
|
|
13
|
+
*/
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { QuoxUserError } from '../core/errors.js';
|
|
16
|
+
import { confirmInteractive } from '../core/confirm.js';
|
|
17
|
+
import { runPreflight, preflightPassed, defaultDeps as preflightDeps } from '../quoxon/onboarding/preflight.js';
|
|
18
|
+
import { defaultInstallDir } from '../quoxon/onboarding/installRunner.js';
|
|
19
|
+
import { passwordProblem, emailOk } from '../quoxon/onboarding/finish.js';
|
|
20
|
+
|
|
21
|
+
export const INSTALL_STAGES = ['preflight', 'docker', 'download', 'compose-up', 'health', 'owner', 'vault', 'link'];
|
|
22
|
+
const FOLD = { fetch: 'download', configure: 'download', build: 'download', start: 'compose-up', health: 'health' };
|
|
23
|
+
export const PLAN_STEPS = [
|
|
24
|
+
'check Docker, Compose, git, openssl, disk, RAM and ports (read-only)',
|
|
25
|
+
'download the installer from get.quox.ai and show its SHA256',
|
|
26
|
+
'clone the Quox source into the install directory',
|
|
27
|
+
'write secrets (.env) and a self-signed certificate',
|
|
28
|
+
'download the Quox images',
|
|
29
|
+
'start the services with docker compose up -d',
|
|
30
|
+
'wait until the auth service answers /health with a 2xx',
|
|
31
|
+
'create your owner account and sign in',
|
|
32
|
+
'print a one-time dashboard sign-in link (10 minutes)'
|
|
33
|
+
];
|
|
34
|
+
const AGAIN = 'quox server install';
|
|
35
|
+
const LINK_HOW = 'quox -p <profile> dashboard (mints a fresh one-time sign-in link), or re-run with --print-login-link';
|
|
36
|
+
|
|
37
|
+
/** The preflight copy is shared with the TUI ("press r"); the CLI/JSON path speaks CLI. */
|
|
38
|
+
const cliWords = (t) => String(t == null ? t : t).replace(/,? then press r to check again\.?/gi, ` Then run: ${AGAIN}`).replace(/,? then press r( to try again)?\.?/gi, '.').replace(/\s+\./g, '.').replace(/\.\s*\./g, '.');
|
|
39
|
+
/** This verb never installs Docker: the honest offer is a manual one with the doc link. */
|
|
40
|
+
const cliRow = (r) => ({
|
|
41
|
+
...r,
|
|
42
|
+
...(r.fix ? { fix: cliWords(r.fix) } : {}),
|
|
43
|
+
...(r.offer === 'install' ? { offer: 'manual' } : {})
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** First line of stdin, no trailing newline. */
|
|
47
|
+
export async function readPasswordStdin(readStdin) {
|
|
48
|
+
const first = String(await readStdin()).split(/\r?\n/)[0];
|
|
49
|
+
if (!first) throw new QuoxUserError('--password-stdin got an empty first line. Pipe the password in, for example: printf %s "$PW" | quox server install --password-stdin', { exitCode: 2, code: 'empty_password' });
|
|
50
|
+
return first;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function defaultReadStdin() {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
56
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function defaultPrompt(message, type) {
|
|
60
|
+
const { default: inquirer } = await import('inquirer');
|
|
61
|
+
const a = await inquirer.prompt([{ type, name: 'v', message, ...(type === 'password' ? { mask: '*' } : {}) }]);
|
|
62
|
+
return a.v;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function defaultMakeInstall({ dir, env = process.env } = {}) {
|
|
66
|
+
const { realInstall } = await import('../quoxon/onboarding/index.js');
|
|
67
|
+
return realInstall({ env, installDir: dir });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* -> { ok, ... }. Throws QuoxUserError (exit 2) for refusals made before anything changes.
|
|
72
|
+
* opts: { dir, dryRun, yes, email, passwordStdin }; deps: see defaults; emit(event).
|
|
73
|
+
*/
|
|
74
|
+
export async function runInstallFlow(opts = {}, deps = {}, emit = () => {}) {
|
|
75
|
+
const d = {
|
|
76
|
+
runChecks: runPreflight, makeInstall: defaultMakeInstall, confirm: confirmInteractive,
|
|
77
|
+
isTTY: () => Boolean(process.stdin.isTTY), readStdin: defaultReadStdin,
|
|
78
|
+
promptEmail: () => defaultPrompt('Owner email', 'input'), promptPassword: () => defaultPrompt('Owner password (hidden)', 'password'),
|
|
79
|
+
...deps
|
|
80
|
+
};
|
|
81
|
+
const { dryRun = false, yes = false, passwordStdin = false } = opts;
|
|
82
|
+
const dir = resolve(opts.dir || defaultInstallDir());
|
|
83
|
+
const printLink = Boolean(opts.printLink);
|
|
84
|
+
const stage = (s, status, extra = {}) => emit({ type: 'stage', stage: s, status, ...extra });
|
|
85
|
+
const fail = (failedStage, error, next, extra = {}) => {
|
|
86
|
+
stage(failedStage, 'fail', { error, next });
|
|
87
|
+
const res = { ok: false, failedStage, error, next, ...extra };
|
|
88
|
+
emit({ type: 'result', ...res });
|
|
89
|
+
return res;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ---- refusals first: nothing has been touched --------------------------------------------------------------
|
|
93
|
+
if (!dryRun) {
|
|
94
|
+
if (!yes && !d.isTTY()) throw new QuoxUserError('No terminal to answer y/N here. Read the plan with --dry-run, then re-run with --yes to install.', { exitCode: 2, code: 'no_terminal' });
|
|
95
|
+
if (!d.isTTY() && !passwordStdin) throw new QuoxUserError('No terminal for the owner password prompt. Pipe it in with --password-stdin (the password is never taken as an argument).', { exitCode: 2, code: 'no_terminal' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- preflight (read-only, also in a dry run) --------------------------------------------------------------
|
|
99
|
+
stage('preflight', 'running');
|
|
100
|
+
const rows = (await d.runChecks(preflightDeps(), () => {})).map(cliRow);
|
|
101
|
+
const dockerRows = rows.filter(r => r.id === 'docker' || r.id === 'compose');
|
|
102
|
+
const otherRows = rows.filter(r => r.id !== 'docker' && r.id !== 'compose');
|
|
103
|
+
const nextFor = (rs) => rs.filter(r => r.status === 'fail').map(r => `${r.fix || r.body}`).join(' ');
|
|
104
|
+
const bad = rows.filter(r => r.status === 'fail');
|
|
105
|
+
const otherBad = bad.filter(r => r.id !== 'docker' && r.id !== 'compose');
|
|
106
|
+
if (otherBad.length) return fail('preflight', bad.map(r => r.body).join('; '), `${nextFor(bad)} Then run: ${AGAIN}`, { checks: rows });
|
|
107
|
+
stage('preflight', 'ok', { detail: otherRows.map(r => r.body).join('; ') });
|
|
108
|
+
if (bad.length) return fail('docker', bad.map(r => r.body).join('; '), `${nextFor(bad)} Then run: ${AGAIN}`, { checks: rows });
|
|
109
|
+
stage('docker', 'ok', { detail: dockerRows.map(r => r.body).join('; ') });
|
|
110
|
+
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
emit({ type: 'plan', dir, installerUrl: 'https://get.quox.ai', checks: rows.map(r => ({ id: r.id, status: r.status, body: r.body })), steps: PLAN_STEPS, changed: 'nothing' });
|
|
113
|
+
const res = { ok: true, dryRun: true };
|
|
114
|
+
emit({ type: 'result', ...res });
|
|
115
|
+
return res;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- confirm, then collect the owner credentials BEFORE the long install -----------------------------------
|
|
119
|
+
if (!yes && !(await d.confirm(`Install Quox into ${dir} now?`, { flag: '--yes', purpose: 'install' }))) {
|
|
120
|
+
const res = { ok: false, declined: true, error: 'not confirmed', next: `Nothing was changed. Run ${AGAIN} --dry-run to see the plan.` };
|
|
121
|
+
emit({ type: 'result', ...res });
|
|
122
|
+
return res;
|
|
123
|
+
}
|
|
124
|
+
let email = opts.email ? String(opts.email).trim() : null;
|
|
125
|
+
if (!email && d.isTTY()) email = String(await d.promptEmail()).trim();
|
|
126
|
+
if (!emailOk(email)) throw new QuoxUserError('A valid owner email is needed: pass --email <address>.', { exitCode: 2, code: 'bad_email' });
|
|
127
|
+
const password = passwordStdin ? await readPasswordStdin(d.readStdin) : await d.promptPassword();
|
|
128
|
+
const weak = passwordProblem(password);
|
|
129
|
+
if (weak) throw new QuoxUserError(`The owner password is not accepted: ${weak}`, { exitCode: 2, code: 'weak_password' });
|
|
130
|
+
|
|
131
|
+
// ---- the install itself (the same runner Getting started uses) ----------------------------------------------
|
|
132
|
+
const { runner, finisher } = await d.makeInstall({ dir });
|
|
133
|
+
const open = {};
|
|
134
|
+
const res = await runner.run({
|
|
135
|
+
onEvent: (e) => {
|
|
136
|
+
if (e.type === 'log') { emit({ type: 'log', line: e.line }); return; }
|
|
137
|
+
if (e.type !== 'stage') return;
|
|
138
|
+
const s = FOLD[e.id]; if (!s) return;
|
|
139
|
+
if (e.status === 'running') { if (!open[s]) { open[s] = true; stage(s, 'running'); } return; }
|
|
140
|
+
if (e.status === 'ok' && (e.id === 'build' || e.id === 'start' || e.id === 'health')) { stage(s, 'ok', e.ms != null ? { ms: e.ms } : {}); open[s] = false; }
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
const failed = FOLD[res.failedStage] || res.failedStage || 'download';
|
|
145
|
+
if (res.interrupted) return fail(failed, 'interrupted', `Run ${AGAIN} again; finished steps are kept.`);
|
|
146
|
+
const next = [res.cause?.next || res.cause?.fix, res.hint || 'Finished steps are kept.', `Then run: ${AGAIN}`].filter(Boolean).join(' ');
|
|
147
|
+
return fail(failed, res.error || 'the installer failed', next, { logPath: res.logPath, ...(res.dashboardUrl ? { dashboardUrl: res.dashboardUrl } : {}) });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- owner account, sign-in, vault, link ---------------------------------------------------------------------
|
|
151
|
+
stage('owner', 'running');
|
|
152
|
+
const made = await finisher.createOwner({ email, password });
|
|
153
|
+
if (!made.ok) {
|
|
154
|
+
return fail('owner', made.error || 'the owner account was not created',
|
|
155
|
+
made.existed ? 'An owner already exists on this server. Sign in with: quox login' : `The server is running. Create the owner with: quox setup`, { dashboardUrl: res.dashboardUrl, logPath: res.logPath });
|
|
156
|
+
}
|
|
157
|
+
const signed = await finisher.signIn({ email, password });
|
|
158
|
+
if (!signed.ok) return fail('owner', `owner created, but sign-in failed: ${signed.error}`, 'Sign in with: quox login', { dashboardUrl: res.dashboardUrl, logPath: res.logPath });
|
|
159
|
+
stage('owner', 'ok', { detail: email });
|
|
160
|
+
stage('vault', 'skip', { detail: 'no AI key was given here', next: 'Add one with: quox provider connect' });
|
|
161
|
+
const link = await finisher.loginLink(res.dashboardUrl);
|
|
162
|
+
if (!link.ok) return fail('link', link.error || 'could not create the sign-in link', `Open ${res.dashboardUrl || 'https://localhost:3000'} and sign in with ${email}.`, { dashboardUrl: res.dashboardUrl, logPath: res.logPath });
|
|
163
|
+
stage('link', 'ok', { ttlSeconds: link.ttlSeconds, linkIssued: true, ...(printLink ? {} : { linkHow: LINK_HOW }) });
|
|
164
|
+
const done = { ok: true, dashboardUrl: res.dashboardUrl, link: link.url, installDir: runner.installDir, logPath: res.logPath };
|
|
165
|
+
const { link: liveLink, ...safe } = done;
|
|
166
|
+
emit({ type: 'result', ...(printLink ? done : { ...safe, linkIssued: true, linkHow: LINK_HOW }) });
|
|
167
|
+
return done;
|
|
168
|
+
}
|
package/lib/server/status.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from 'node:fs';
|
|
6
6
|
import { join, dirname } from 'node:path';
|
|
7
|
+
import { X509Certificate } from 'node:crypto';
|
|
7
8
|
import { readInstalled } from './install.js';
|
|
8
9
|
|
|
9
10
|
const STATE_FILE = 'lifecycle-state.json';
|
|
@@ -67,6 +68,18 @@ async function getJson(url, fetchImpl, timeout = 3000) {
|
|
|
67
68
|
} catch (e) { return { ok: false, error: e?.cause?.code || e?.message || 'no answer' }; }
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* H3: the SHA256 fingerprint of the certificate the edge serves (ssl/quox.crt in the checkout), in the same format
|
|
73
|
+
* as the `quox connect` card, so the two can be compared by eye or by script. Never throws.
|
|
74
|
+
*/
|
|
75
|
+
export function certFingerprint(repoDir, { read = readFileSync } = {}) {
|
|
76
|
+
const file = join(repoDir || '', 'ssl', 'quox.crt');
|
|
77
|
+
try {
|
|
78
|
+
const c = new X509Certificate(read(file));
|
|
79
|
+
return { ok: true, file, fingerprint256: c.fingerprint256, validTo: c.validTo };
|
|
80
|
+
} catch (e) { return { ok: false, file, error: e?.code === 'ENOENT' ? 'no certificate file' : 'could not read the certificate' }; }
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/** The running build, from the collector's public /version. */
|
|
71
84
|
export async function runningVersion(collectorUrl, fetchImpl = globalThis.fetch) {
|
|
72
85
|
const r = await getJson(`${collectorUrl.replace(/\/+$/, '')}/version`, fetchImpl);
|
|
@@ -99,7 +112,7 @@ export async function gatherStatus({ install, sys, fetchImpl = globalThis.fetch,
|
|
|
99
112
|
const hist = backups ? readHistory(backups) : [];
|
|
100
113
|
const lastUpdate = [...hist].reverse().find((e) => e.op === 'update' || e.op === 'rollback') || null;
|
|
101
114
|
const state = backups ? readState(backups) : null;
|
|
102
|
-
return { install, installed, running, services: svc, disk, http, lastUpdate, inFlight: state && state.status === 'running' ? state : null };
|
|
115
|
+
return { install, installed, running, services: svc, disk, http, cert: certFingerprint(install.repoDir), lastUpdate, inFlight: state && state.status === 'running' ? state : null };
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
const svcIcon = (s) => (s.state === 'running' ? (s.health && s.health !== 'healthy' ? '⚠' : '✔') : '✖');
|
|
@@ -113,6 +126,7 @@ export function statusRows(st) {
|
|
|
113
126
|
out.push(row('Version', `${i.release || '? no VERSION file'}${i.tag ? ` (${i.tag})` : ''} · checkout ${i.commit || '?'}${i.dirty ? ' · local changes' : ''}`));
|
|
114
127
|
const rv = st.running;
|
|
115
128
|
out.push(row('Running', rv.ok ? `${rv.commit || '?'}${rv.stamped === false ? ' (unstamped build)' : ''}${rv.commit && i.commit && !String(rv.commit).startsWith(i.commit) && !String(i.commit).startsWith(rv.commit) ? ' · differs from the checkout' : ''}` : `? ${rv.error}`));
|
|
129
|
+
if (st.cert) out.push(row('Certificate', st.cert.ok ? `SHA256 ${st.cert.fingerprint256} · valid to ${st.cert.validTo} (compare with the card \`quox connect\` shows)` : `? ${st.cert.error} (${st.cert.file})`));
|
|
116
130
|
for (const h of st.http) out.push(row(`${h.ok ? '✔' : '✖'} ${h.name}`, `${h.url} · ${h.detail} · ${h.ms}ms`));
|
|
117
131
|
if (!st.services.ok) out.push(row('✖ Services', `? ${st.services.error}`));
|
|
118
132
|
else {
|
package/lib/toolOutputShape.js
CHANGED
|
@@ -270,6 +270,59 @@ function smallTopFields(body, found) {
|
|
|
270
270
|
return Object.keys(out).length ? out : null;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* OVERSIZED-IDS: the oversized summary used to carry only a sample of whole rows, which a big row (logs, blobs)
|
|
276
|
+
* pushes out entirely, so the model lost every id/name and answered from another tool (keeper_backup_list,
|
|
277
|
+
* bob_log_tail). id_index keeps the first ID_INDEX_ROWS rows of every list (top level, or under a top key) reduced
|
|
278
|
+
* to identifier/status fields, inside ID_INDEX_BUDGET chars in total, and says how many rows were left out.
|
|
279
|
+
*/
|
|
280
|
+
export const ID_INDEX_ROWS = 20;
|
|
281
|
+
export const ID_INDEX_BUDGET = 6000;
|
|
282
|
+
const ID_FIELD_RE = /^(id|name|title|status|state|created_at|started_at|key|host|host_id|hostname|slug|label|kind|type)$|_id$/i;
|
|
283
|
+
const ID_VALUE_MAX = 80;
|
|
284
|
+
|
|
285
|
+
function idRow(row) {
|
|
286
|
+
if (!isObj(row)) return (row === null || typeof row === 'object') ? null : (typeof row === 'string' ? row.slice(0, ID_VALUE_MAX) : row);
|
|
287
|
+
const r = {};
|
|
288
|
+
for (const [k, v] of Object.entries(row)) {
|
|
289
|
+
if (BLOCKED.has(k) || !ID_FIELD_RE.test(k)) continue;
|
|
290
|
+
if (v === null || typeof v === 'object') continue;
|
|
291
|
+
r[k] = typeof v === 'string' && v.length > ID_VALUE_MAX ? `${v.slice(0, ID_VALUE_MAX)}...` : v;
|
|
292
|
+
}
|
|
293
|
+
return r;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function idIndex(body, found) {
|
|
297
|
+
const lists = [];
|
|
298
|
+
if (Array.isArray(body)) lists.push(['(root)', body]);
|
|
299
|
+
else if (isObj(body)) {
|
|
300
|
+
for (const [k, v] of Object.entries(body)) if (!BLOCKED.has(k) && Array.isArray(v) && v.length) lists.push([k, v]);
|
|
301
|
+
if (!lists.length && found && found.path) lists.push([found.path, found.list]);
|
|
302
|
+
}
|
|
303
|
+
if (!lists.length) return null;
|
|
304
|
+
// the main list first, then the rest by size
|
|
305
|
+
const mainPath = found && found.path;
|
|
306
|
+
lists.sort((a, b) => ((b[0] === mainPath) - (a[0] === mainPath)) || b[1].length - a[1].length);
|
|
307
|
+
const out = {};
|
|
308
|
+
let used = 0;
|
|
309
|
+
for (const [path, list] of lists.slice(0, 8)) {
|
|
310
|
+
const rows = [];
|
|
311
|
+
for (const it of list.slice(0, ID_INDEX_ROWS)) {
|
|
312
|
+
const r = idRow(it);
|
|
313
|
+
if (r === null || (isObj(r) && !Object.keys(r).length)) continue;
|
|
314
|
+
const n = jsonLen(r) + 2;
|
|
315
|
+
if (used + n + 120 > ID_INDEX_BUDGET) break;
|
|
316
|
+
rows.push(r);
|
|
317
|
+
used += n;
|
|
318
|
+
}
|
|
319
|
+
if (!rows.length) continue;
|
|
320
|
+
used += 120;
|
|
321
|
+
out[path] = { total: list.length, shown: rows.length, omitted: list.length - rows.length, rows };
|
|
322
|
+
}
|
|
323
|
+
return Object.keys(out).length ? out : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
273
326
|
export function capInline(value, { maxChars = INLINE_MAX_CHARS } = {}) {
|
|
274
327
|
const size = jsonLen(value);
|
|
275
328
|
if (size <= maxChars) return { value, capped: false };
|
|
@@ -299,6 +352,8 @@ export function capInline(value, { maxChars = INLINE_MAX_CHARS } = {}) {
|
|
|
299
352
|
: `--select '${lp ? `${lp}[field=value].name` : '[0]'}', --select 'count(${lp || ''})' or --select '${lp ? `${lp}[*].field` : '[*].field'}'`;
|
|
300
353
|
const next = `Result is ${fullKb} KB, too large to show whole, so this is the --summary view. For the exact part you need, run the same quox tool exec again with ${example} (or --summary --limit N for a bigger sample)${via ? `; the list is inside ${via}` : ''}. Never read or post-process a saved output with a shell.`;
|
|
301
354
|
const head = { oversized: true, full_size_kb: fullKb, next, top_keys: isObj(body) ? Object.keys(body).slice(0, 40) : undefined };
|
|
355
|
+
const idx = idIndex(body, found);
|
|
356
|
+
if (idx) head.id_index = idx;
|
|
302
357
|
const extra = smallTopFields(body, found);
|
|
303
358
|
for (const limit of [DEFAULT_SAMPLE, 5, 3, 1, 0]) {
|
|
304
359
|
const s = summarize(body, { limit });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quoxcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Quox: the conversational front door to your self-hosted AI agent control plane",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"files": [
|
|
54
54
|
"bin/",
|
|
55
55
|
"lib/",
|
|
56
|
+
"vendor/",
|
|
56
57
|
"!lib/**/*.test.js",
|
|
57
58
|
"!lib/**/*.test.jsx",
|
|
58
59
|
"!lib/**/__tests__/",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"!lib/core/websiteSession.js",
|
|
61
62
|
"!lib/quoxon/whatsnew.md",
|
|
62
63
|
"README.md",
|
|
64
|
+
"CHANGELOG.md",
|
|
63
65
|
"LICENSE"
|
|
64
66
|
],
|
|
65
67
|
"repository": {
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
"url": "git+https://github.com/quoxai/quoxcore.git",
|
|
68
70
|
"directory": "cli"
|
|
69
71
|
},
|
|
70
|
-
"homepage": "https://
|
|
72
|
+
"homepage": "https://quox.ai/cli",
|
|
71
73
|
"bugs": {
|
|
72
74
|
"url": "https://github.com/quoxai/quoxcore/issues"
|
|
73
75
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
|
|
2
|
+
<title>Cipher — network operations, electric-blue listening dragon</title>
|
|
3
|
+
<g id="wings">
|
|
4
|
+
<path id="far-sail" fill="#185a9b" d="M 352 288 Q 363 205 445 105 L 430 222 L 484 181 L 451 285 Z"/>
|
|
5
|
+
<path id="near-sail" fill="#297cc4" d="M 398 302 Q 450 189 564 144 L 531 227 L 579 209 L 537 272 L 572 272 L 483 329 Z"/>
|
|
6
|
+
<path id="sail-ribs" fill="none" stroke="#81ddff" stroke-width="8" d="M 398 302 L 564 144 L 481 272 L 537 272 M 352 288 L 445 105 L 398 246"/>
|
|
7
|
+
</g>
|
|
8
|
+
<g id="tail">
|
|
9
|
+
<path id="tail-ribbon" fill="#2988da" d="M 475 305 C 538 278 625 287 658 327 C 696 378 630 409 589 387 C 547 366 564 328 613 313 C 673 294 711 263 695 218 Q 683 189 654 197 Q 691 201 677 234 C 663 262 600 266 559 295 C 502 337 525 398 576 419 C 644 446 731 395 696 332 C 663 266 548 253 469 275 Z"/>
|
|
10
|
+
<path id="tail-edge" fill="none" stroke="#9aefff" stroke-width="7" d="M 491 294 C 554 274 641 289 665 335 C 686 374 637 404 594 384 M 613 313 C 673 294 711 263 695 218"/>
|
|
11
|
+
<path id="tail-fin" fill="#76dbfb" d="M 688 226 L 641 221 L 658 202 L 635 180 Q 688 178 699 215 Z"/>
|
|
12
|
+
</g>
|
|
13
|
+
<g id="body">
|
|
14
|
+
<path id="torso" fill="#287fc3" d="M 288 246 C 328 225 374 246 402 266 C 445 267 494 287 510 317 Q 524 356 476 367 L 331 354 Q 282 335 278 291 Z"/>
|
|
15
|
+
<path id="silver-belly" fill="#bddceb" d="M 291 265 Q 346 301 426 308 L 480 340 Q 405 365 336 337 Q 295 319 291 265 Z"/>
|
|
16
|
+
<path id="belly-plates" fill="none" stroke="#477b9d" stroke-width="5" d="M 320 295 L 315 325 M 348 306 L 344 339 M 379 311 L 376 345 M 410 315 L 408 348"/>
|
|
17
|
+
<path id="rear-legs" fill="#2367a8" d="M 455 314 Q 496 299 505 338 L 495 386 L 526 399 L 525 412 L 468 412 L 469 362 L 443 347 Z M 388 329 L 405 376 L 435 391 L 429 405 L 382 402 L 366 345 Z"/>
|
|
18
|
+
<path id="front-legs" fill="#409bde" d="M 298 290 Q 323 283 332 307 L 322 378 L 349 400 L 345 416 L 293 415 Q 283 409 290 390 L 291 330 Z M 265 288 L 281 312 L 265 378 L 280 393 L 270 408 L 231 405 L 243 371 Z"/>
|
|
19
|
+
<path id="talons" fill="#e8f7ff" d="M 296 403 L 289 417 L 305 412 L 313 401 L 311 417 L 327 412 L 335 404 L 337 417 L 350 411 M 474 400 L 466 413 L 483 410 L 490 399 L 490 414 L 507 410 L 515 400 L 519 413 L 531 408 M 235 393 L 230 406 L 247 402 L 255 392 L 255 408 L 272 401"/>
|
|
20
|
+
<path id="dorsal-fins" fill="#7ae6ff" d="M 330 249 L 341 224 L 356 258 L 374 238 L 386 269 L 409 251 L 421 276 L 445 260 L 457 289"/>
|
|
21
|
+
</g>
|
|
22
|
+
<g id="neck">
|
|
23
|
+
<path id="neck-mass" fill="#358bd1" d="M 230 180 Q 281 156 310 193 Q 316 231 349 273 Q 305 315 263 281 Q 271 239 230 180 Z"/>
|
|
24
|
+
<path id="neck-silver" fill="#d1e8f4" d="M 237 201 Q 259 214 274 233 Q 281 259 274 282 Q 295 297 314 284 Q 294 249 288 221 Z"/>
|
|
25
|
+
</g>
|
|
26
|
+
<g id="head">
|
|
27
|
+
<path id="antenna-far" fill="none" stroke="#9eeeff" stroke-width="9" d="M 233 127 Q 225 82 263 46 L 283 22"/>
|
|
28
|
+
<path id="antenna-near" fill="none" stroke="#d5f5ff" stroke-width="10" d="M 278 130 Q 286 74 332 55 L 366 33"/>
|
|
29
|
+
<path id="ear-far" fill="#5dc8f5" d="M 216 146 L 166 106 L 185 161 L 165 172 L 218 179 Z"/>
|
|
30
|
+
<path id="head-mass" fill="#469ee0" d="M 203 143 Q 217 112 263 116 Q 301 117 312 152 Q 324 189 295 217 Q 255 245 216 218 L 192 188 Z"/>
|
|
31
|
+
<path id="ear-near" fill="#70d8fa" d="M 291 153 L 354 113 L 332 157 L 362 150 L 334 185 L 347 189 L 302 205 Z"/>
|
|
32
|
+
<path id="ear-cut" fill="none" stroke="#2978bb" stroke-width="5" d="M 305 169 L 337 139 M 309 182 L 336 168"/>
|
|
33
|
+
<path id="muzzle" fill="#9bd7ed" d="M 208 181 Q 187 177 178 190 Q 166 208 191 220 Q 229 238 266 218 L 270 204 Q 237 215 208 181 Z"/>
|
|
34
|
+
<path id="nose" fill="#080d19" d="M 185 194 Q 195 186 202 194 Q 197 202 185 199 Z"/>
|
|
35
|
+
<path id="smile" fill="none" stroke="#164668" stroke-width="5" d="M 185 211 Q 218 231 255 214"/>
|
|
36
|
+
<path id="brow" fill="none" stroke="#a6edff" stroke-width="6" d="M 213 143 Q 230 130 245 143 M 259 140 Q 280 125 295 141"/>
|
|
37
|
+
</g>
|
|
38
|
+
<g id="eyes">
|
|
39
|
+
<path id="far-eye-socket" fill="#080d19" d="M 210 154 Q 223 138 240 149 Q 253 164 243 184 Q 222 197 211 177 Z"/>
|
|
40
|
+
<path id="far-eye-white" fill="#ffffff" d="M 216 156 Q 227 145 237 155 Q 246 166 238 179 Q 224 188 216 174 Z"/>
|
|
41
|
+
<path id="far-pupil" fill="#080d19" d="M 225 166 C 225 157 234 157 234 166 C 234 175 225 175 225 166 Z"/>
|
|
42
|
+
<path id="near-eye-socket" fill="#080d19" d="M 253 153 Q 272 134 293 149 Q 310 168 295 190 Q 271 204 254 183 Z"/>
|
|
43
|
+
<path id="near-eye-white" fill="#ffffff" d="M 260 155 Q 275 142 289 154 Q 301 169 290 184 Q 274 194 261 179 Z"/>
|
|
44
|
+
<path id="near-pupil" fill="#080d19" d="M 272 168 C 272 157 283 157 283 168 C 283 179 272 179 272 168 Z"/>
|
|
45
|
+
</g>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
2
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
3
|
+
const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
|
|
4
|
+
export const CIPHER_CLIPS=[{id:'idle',title:'Listening',duration:12000},{id:'attach',title:'Link Up',duration:3200},{id:'inspect',title:'Following Traffic',duration:3600},{id:'trace',title:'Trace Route',duration:4800},{id:'delay',title:'Delayed Hop',duration:4800},{id:'broken',title:'Broken Link',duration:4800},{id:'restored',title:'Route Restored',duration:4800}];
|
|
5
|
+
export const STORY=[['attach',3200],['idle',1600],['inspect',3600],['trace',4800],['delay',4800],['broken',4800],['restored',4800],['idle',2400]];
|
|
6
|
+
export const STORY_DURATION=STORY.reduce((s,b)=>s+b[1],0);
|
|
7
|
+
export function poseRig(rig,t,scene){
|
|
8
|
+
const cycle=t%12000,arrival=scene==='attach'&&t<3200;
|
|
9
|
+
const attention=arrival?pulse(t,0,2800):pulse(cycle,600,3400);
|
|
10
|
+
const fin=arrival?pulse(t,500,2900):pulse(cycle,2600,4300);
|
|
11
|
+
const tail=arrival?pulse(t,1000,3200):pulse(cycle,6200,10500);
|
|
12
|
+
const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
13
|
+
if(p.id.endsWith('-pupil'))x+=(scene==='inspect'?Math.sin(t/700):attention)*2;
|
|
14
|
+
if(p.group==='head'||p.group==='eyes'){x+=attention*3;y-=attention*3;if(p.id.startsWith('ear-'))y-=fin*4*ease((x-290)/65);if(p.id.startsWith('antenna-'))x+=fin*4*ease((130-y)/100);}
|
|
15
|
+
if(p.group==='neck'){const w=ease((290-y)/100);x+=attention*3*w;y-=attention*3*w;}
|
|
16
|
+
if(p.group==='wings'){const w=ease((300-y)/170);x-=fin*3*w;y+=fin*3*w;}
|
|
17
|
+
if(p.group==='tail'){const w=ease((x-490)/150);x-=tail*4*w;y+=tail*4*w*Math.sin((x-490)/70);}
|
|
18
|
+
return [x,y];
|
|
19
|
+
}))}));
|
|
20
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
21
|
+
return {...rig,paths,bounds:{left:rig.bounds.left-7,top:rig.bounds.top-7,right:rig.bounds.right+7,bottom:rig.bounds.bottom+7},eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
22
|
+
}
|
|
23
|
+
function route(frame,t,scene,mono){
|
|
24
|
+
const cols=frame[0].length,rows=frame.length,y=1;
|
|
25
|
+
const nodes=[.53,.69,.85].map(f=>Math.round(f*(cols-1)));
|
|
26
|
+
let effectCells=0;
|
|
27
|
+
const put=(x,ch,rgb)=>{if(x<1||x>=cols-1||y>=rows-1||frame[y][x].ch!==' ')return;frame[y][x]={ch,rgb:mono?null:rgb,bg:null};effectCells++;};
|
|
28
|
+
const cyan=[103,226,255],amber=[255,194,76],red=[255,100,103];
|
|
29
|
+
const p=Math.min(1,t/4000),blocked=scene==='broken',delayed=scene==='delay';
|
|
30
|
+
nodes.forEach((x,i)=>put(x,'⠿',(i===1&&(blocked||delayed))?(blocked?red:amber):cyan));
|
|
31
|
+
for(let x=nodes[0];x<=nodes[2];x++){if(blocked&&x>nodes[1]&&x<nodes[1]+3)continue;put(x,'⠒',cyan);}
|
|
32
|
+
// Replace our route cells only; never overwrite original art.
|
|
33
|
+
const set=(x,ch,rgb)=>{if(x>=nodes[0]&&x<=nodes[2]&&['⠒','⠿'].includes(frame[y][x].ch))frame[y][x]={ch,rgb:mono?null:rgb,bg:null};};
|
|
34
|
+
let progress=p;if(delayed)progress=p<.3?p/.6:p<.7?.5:.5+(p-.7)/.6;if(blocked)progress=Math.min(.5,p);
|
|
35
|
+
const x=Math.round(nodes[0]+(nodes[2]-nodes[0])*Math.min(1,progress));
|
|
36
|
+
set(x,'⣿',blocked?red:delayed&&progress===.5?amber:[255,255,255]);
|
|
37
|
+
return effectCells;
|
|
38
|
+
}
|
|
39
|
+
export function renderCipher(rig,scene='idle',ms=0,options={}){
|
|
40
|
+
const clip=CIPHER_CLIPS.find(c=>c.id===scene);if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
|
|
41
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
42
|
+
const t=options.still?0:ms;
|
|
43
|
+
const idleT=scene==='attach'&&t>=3200?t-3200:t;
|
|
44
|
+
const posed=poseRig(rig,options.still?0:(options.motionTime??idleT),scene==='attach'&&t>=3200?'idle':scene);
|
|
45
|
+
const r=renderSvgRig(posed,options),power=['trace','delay','broken','restored'].includes(scene);
|
|
46
|
+
const effectCells=power?route(r.frame,options.still?2400:t,scene,options.mono):0;
|
|
47
|
+
return {...r,scene,clip:clip.title,caption:power?clip.title+' · simulated network state':'Listening to the network.',effectCells};
|
|
48
|
+
}
|
|
49
|
+
export function renderStory(rig,ms,options={}){let t=options.still?0:Math.min(ms,STORY_DURATION-1);for(const [scene,duration]of STORY){if(t<duration)return renderCipher(rig,scene,t,{...options});t-=duration;}}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
import {renderCipher,CIPHER_CLIPS} from './character.mjs';
|
|
5
|
+
import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
|
|
6
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
7
|
+
const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
|
|
8
|
+
for(let i=0;i<args.length;i++){
|
|
9
|
+
if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
|
|
10
|
+
else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
|
|
11
|
+
}
|
|
12
|
+
const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
|
|
13
|
+
if(args.includes('--help')){console.log('Cipher V2 acting studio. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
|
|
14
|
+
if(args.includes('--list')){console.log(CIPHER_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
|
|
15
|
+
const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
|
|
16
|
+
if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
|
|
17
|
+
const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
|
|
18
|
+
const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
|
|
19
|
+
const message=value('--message','CIPHER\nOn guard.');
|
|
20
|
+
if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
|
|
21
|
+
else{
|
|
22
|
+
if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
|
|
23
|
+
const rig=parseSvgRig(await fs.readFile(new URL('../v1/cipher-master.svg',import.meta.url),'utf8'));
|
|
24
|
+
const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
|
|
25
|
+
const scene=value('--scene',args.includes('--attach')?'attach':'idle');
|
|
26
|
+
const r=renderCipher(rig,scene,Number(value('--at',0)),opts);
|
|
27
|
+
if(!r.ok)throw Error(r.code);
|
|
28
|
+
if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
|
|
29
|
+
const {watch}=await import('./watch.mjs');
|
|
30
|
+
await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
|
|
31
|
+
}else
|
|
32
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {CIPHER_CLIPS,renderCipher,renderStory,STORY_DURATION} from './character.mjs';
|
|
3
|
+
import {composeMessage} from '../../lucid/recovery/layout.mjs';
|
|
4
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
5
|
+
|
|
6
|
+
/** One bounded performance. No alternate screen or full-screen clearing. */
|
|
7
|
+
export async function watch(paths,options,config){
|
|
8
|
+
if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
|
|
9
|
+
const {terminalCols,terminalRows,scene,large,message}=config;
|
|
10
|
+
const clip=scene?CIPHER_CLIPS.find(c=>c.id===scene):null;
|
|
11
|
+
const duration=(config.arrival&&options.deck?3200:clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
|
|
12
|
+
const realCols=process.stdout.columns,realRows=process.stdout.rows;
|
|
13
|
+
const start=performance.now();
|
|
14
|
+
const render=t=>{
|
|
15
|
+
const r=scene?renderCipher(paths,scene,t,options):renderStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'CIPHER · '+r.clip+'\n'+r.caption;
|
|
18
|
+
return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
|
|
19
|
+
};
|
|
20
|
+
// Reserve enough lines for captions too; keep within the real terminal.
|
|
21
|
+
const height=options.rows+3;
|
|
22
|
+
if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
|
|
23
|
+
let ended=false,drawn=false,timer,resolveDone,rejectDone;
|
|
24
|
+
const wasRaw=Boolean(process.stdin.isRaw);
|
|
25
|
+
const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
|
|
26
|
+
const cleanup=(error)=>{
|
|
27
|
+
if(ended)return;ended=true;clearInterval(timer);
|
|
28
|
+
process.stdin.off('data',key);process.stdout.off('resize',resized);
|
|
29
|
+
process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
|
|
30
|
+
process.stdin.setRawMode(wasRaw);process.stdin.pause();
|
|
31
|
+
if(drawn)process.stdout.write(`\x1b[${height}B\r`);
|
|
32
|
+
process.stdout.write('\x1b[0m\x1b[?25h');
|
|
33
|
+
error?rejectDone(error):resolveDone();
|
|
34
|
+
};
|
|
35
|
+
const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
|
|
36
|
+
const sigint=()=>cleanup(),sigterm=()=>cleanup();
|
|
37
|
+
const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
|
|
38
|
+
try{
|
|
39
|
+
process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
|
|
40
|
+
process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
|
|
41
|
+
process.stdout.write('\x1b[?25l');
|
|
42
|
+
// Allocate the block once; each frame replaces only these lines.
|
|
43
|
+
process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
|
|
44
|
+
const tick=()=>{
|
|
45
|
+
try{
|
|
46
|
+
if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
|
|
47
|
+
const t=Math.min(performance.now()-start,duration-1),lines=render(t);
|
|
48
|
+
if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
|
|
49
|
+
process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
|
|
50
|
+
if(performance.now()-start>=duration)cleanup();
|
|
51
|
+
}catch(e){cleanup(e);}
|
|
52
|
+
};
|
|
53
|
+
tick();if(!ended)timer=setInterval(tick,1000/6);
|
|
54
|
+
await done;
|
|
55
|
+
}catch(e){cleanup(e);await done;}
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
2
|
+
import {flattenPath} from '../../stage3/svg-coverage.js';
|
|
3
|
+
export const DRAGO_CLIPS=Object.freeze(['idle','active']);
|
|
4
|
+
const glare={
|
|
5
|
+
'far-eye-white':'M 325 113 L 358 128 L 357 141 Q 336 138 325 124 Z',
|
|
6
|
+
'near-eye-white':'M 382 128 L 424 111 L 422 126 Q 405 139 383 141 Z',
|
|
7
|
+
'far-pupil':'M 341 128 C 341 122 348 122 348 128 C 348 134 341 134 341 128 Z',
|
|
8
|
+
'near-pupil':'M 398 128 C 398 122 405 122 405 128 C 405 134 398 134 398 128 Z'
|
|
9
|
+
};
|
|
10
|
+
export function poseDrago(rig,scene='idle'){
|
|
11
|
+
if(!DRAGO_CLIPS.includes(scene))throw RangeError('Unknown Drago scene: '+scene);
|
|
12
|
+
const paths=rig.paths.map(p=>scene==='active'&&glare[p.id]?{...p,polygons:flattenPath(glare[p.id])}:p);
|
|
13
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
14
|
+
return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
15
|
+
}
|
|
16
|
+
export function renderDrago(rig,scene='idle',ms=0,options={}){
|
|
17
|
+
if(!Number.isFinite(ms)||ms<0)throw RangeError('Invalid time');
|
|
18
|
+
return {...renderSvgRig(poseDrago(rig,scene),options),scene,staticStudy:true,caption:'DRAGO\n'+(scene==='active'?'Active glare study.':'On guard.')};
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
|
|
2
|
+
<title>Drago — crimson engagement guardian, shield-bearing terminal master</title>
|
|
3
|
+
<g id="wings">
|
|
4
|
+
<path id="left-wing" fill="#922837" d="M 349 289 C 286 246 246 199 224 146 Q 208 115 183 103 C 197 184 158 282 109 365 Q 156 334 181 348 Q 218 308 246 340 Q 275 308 302 350 Z"/>
|
|
5
|
+
<path id="left-wing-ribs" fill="none" stroke="#ef5350" stroke-width="8" d="M 349 289 Q 240 207 183 103 M 183 103 Q 201 242 109 365 M 204 178 Q 235 272 181 348 M 232 217 L 246 340"/>
|
|
6
|
+
<path id="right-wing" fill="#6d2433" d="M 445 266 Q 520 206 566 108 Q 585 80 605 83 C 585 165 628 273 687 361 Q 647 333 623 349 Q 589 305 563 332 Q 529 301 501 347 Z"/>
|
|
7
|
+
<path id="right-wing-ribs" fill="none" stroke="#d34243" stroke-width="9" d="M 445 266 Q 546 167 605 83 M 605 83 Q 583 203 687 361 M 584 153 Q 554 234 623 349 M 556 194 L 563 332"/>
|
|
8
|
+
<path id="wing-claws" fill="#ffc28e" d="M 180 121 Q 162 93 192 86 L 183 103 Z M 593 100 Q 600 69 621 74 L 605 91 Z"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g id="body">
|
|
11
|
+
<path id="haunches" fill="#b53b43" d="M 346 250 C 293 266 267 326 278 380 Q 291 422 342 420 L 476 420 Q 536 416 543 372 C 544 311 493 259 450 242 Z"/>
|
|
12
|
+
<path id="neck" fill="#c34143" d="M 348 146 C 335 205 327 243 310 294 Q 382 324 473 285 C 438 245 442 195 432 146 Z"/>
|
|
13
|
+
<path id="throat" fill="#e9ac8a" d="M 361 166 Q 358 233 339 280 Q 388 307 435 278 Q 414 224 410 166 Z"/>
|
|
14
|
+
<path id="throat-plates" fill="none" stroke="#79303b" stroke-width="6" d="M 359 199 Q 386 211 415 197 M 356 221 Q 387 235 420 221 M 350 245 Q 387 258 426 245 M 344 267 Q 388 281 433 267"/>
|
|
15
|
+
<path id="spine" fill="#ff6048" d="M 433 156 L 473 160 L 447 180 L 485 197 L 448 206 L 481 230 L 451 232 L 491 262 L 464 260 L 510 287 L 481 288 Z"/>
|
|
16
|
+
<path id="feet" fill="#bd4249" d="M 301 357 Q 278 379 244 398 Q 230 420 262 428 L 329 428 L 344 383 Z M 481 362 Q 518 375 548 407 Q 552 428 523 429 L 463 429 L 450 384 Z"/>
|
|
17
|
+
<path id="foot-talons" fill="#f5c59d" d="M 251 410 L 243 430 L 267 422 L 278 409 L 274 431 L 295 421 L 306 409 L 307 430 L 326 421 M 477 410 L 467 431 L 489 423 L 500 411 L 500 433 L 520 423 L 531 414 L 535 431 L 548 424"/>
|
|
18
|
+
</g>
|
|
19
|
+
<g id="head">
|
|
20
|
+
<path id="far-horn" fill="#f5b47f" d="M 343 107 C 309 93 296 63 290 27 Q 320 65 365 70 Z"/>
|
|
21
|
+
<path id="near-horn" fill="#ffc18b" d="M 410 91 C 459 81 482 50 502 18 C 499 64 462 104 430 121 Z"/>
|
|
22
|
+
<path id="skull" fill="#d84a49" d="M 323 112 Q 320 79 362 75 Q 412 65 437 101 L 444 145 Q 425 184 380 188 Q 329 183 312 149 Z"/>
|
|
23
|
+
<path id="brow-armour" fill="#f06956" d="M 324 104 L 349 78 L 366 86 L 386 62 L 401 84 L 426 79 L 438 106 L 395 121 L 365 118 Z"/>
|
|
24
|
+
<path id="cheek-blades" fill="#ff7458" d="M 434 119 L 478 100 L 453 132 L 480 134 L 447 152 L 461 168 L 425 169 Z M 321 119 L 294 105 L 303 132 L 288 137 L 315 153 Z"/>
|
|
25
|
+
<path id="muzzle" fill="#e0715b" d="M 328 145 Q 357 135 379 150 Q 401 142 422 151 L 414 174 Q 379 196 332 178 L 313 164 Z"/>
|
|
26
|
+
<path id="nose" fill="#080d19" d="M 329 153 L 341 151 L 336 158 Z M 400 153 L 411 151 L 404 159 Z"/>
|
|
27
|
+
<path id="mouth" fill="none" stroke="#080d19" stroke-width="5" d="M 327 172 Q 370 184 414 168"/>
|
|
28
|
+
<path id="fangs" fill="#fff0d4" d="M 337 176 L 346 193 L 350 179 Z M 398 176 L 402 188 L 409 172 Z"/>
|
|
29
|
+
</g>
|
|
30
|
+
<g id="shield">
|
|
31
|
+
<path id="shield-outer" fill="#ff7058" d="M 386 260 Q 430 289 492 294 L 486 350 Q 471 401 390 440 Q 312 401 296 350 L 290 294 Q 347 290 386 260 Z"/>
|
|
32
|
+
<path id="shield-dark" fill="#080d19" d="M 386 273 Q 435 301 478 303 L 473 348 Q 456 391 390 425 Q 327 391 309 348 L 304 303 Q 352 298 386 273 Z"/>
|
|
33
|
+
<path id="shield-left" fill="#bd3841" d="M 385 285 L 385 411 Q 333 381 321 346 L 317 314 Q 355 307 385 285 Z"/>
|
|
34
|
+
<path id="shield-right" fill="#842d3d" d="M 394 286 Q 431 309 466 314 L 461 345 Q 448 382 394 411 Z"/>
|
|
35
|
+
<path id="shield-rune" fill="none" stroke="#ffc999" stroke-width="8" d="M 360 334 L 389 354 L 418 334 M 389 354 L 389 386"/>
|
|
36
|
+
</g>
|
|
37
|
+
<g id="hands">
|
|
38
|
+
<path id="left-arm" fill="#d74b4a" d="M 315 264 Q 279 270 283 298 L 317 321 L 337 304 L 312 288 Z"/>
|
|
39
|
+
<path id="right-arm" fill="#ba3c43" d="M 454 260 Q 501 265 501 296 L 464 320 L 442 305 L 475 286 Z"/>
|
|
40
|
+
<path id="gripping-talons" fill="#ffe0b2" d="M 311 297 Q 322 286 333 301 L 330 321 L 323 305 L 318 323 Z M 329 297 Q 342 291 348 306 L 343 323 L 338 307 Z M 445 297 Q 457 290 462 303 L 456 324 L 451 308 Z M 464 297 Q 478 288 482 302 L 473 322 L 471 305 Z"/>
|
|
41
|
+
</g>
|
|
42
|
+
<g id="tail">
|
|
43
|
+
<path id="tail-coil" fill="#aa3541" d="M 501 350 C 554 351 608 371 605 405 C 601 440 507 464 392 460 C 271 458 190 439 180 406 Q 169 376 199 359 Q 178 392 232 407 C 326 438 473 431 535 411 Q 564 393 502 384 Z"/>
|
|
44
|
+
<path id="tail-rim" fill="none" stroke="#ed5a50" stroke-width="9" d="M 199 359 Q 178 392 232 407 C 326 438 473 431 535 411"/>
|
|
45
|
+
<path id="tail-tip" fill="#ff815e" d="M 201 359 L 169 346 L 180 379 L 162 390 L 195 395 Q 181 376 201 359 Z"/>
|
|
46
|
+
<path id="tail-scutes" fill="none" stroke="#752939" stroke-width="6" d="M 226 410 L 218 434 M 264 420 L 257 444 M 305 428 L 302 453 M 351 434 L 350 458 M 400 435 L 402 459 M 451 432 L 459 457 M 500 425 L 514 449 M 543 410 L 563 432"/>
|
|
47
|
+
</g>
|
|
48
|
+
<g id="eyes">
|
|
49
|
+
<path id="far-eye-socket" fill="#080d19" d="M 321 111 Q 340 100 362 114 L 363 145 Q 336 154 320 134 Z"/>
|
|
50
|
+
<path id="far-eye-white" fill="#ffffff" d="M 326 115 Q 342 109 357 118 L 357 139 Q 338 147 326 131 Z"/>
|
|
51
|
+
<path id="far-pupil" fill="#080d19" d="M 338 127 C 338 117 348 117 348 127 C 348 137 338 137 338 127 Z"/>
|
|
52
|
+
<path id="near-eye-socket" fill="#080d19" d="M 377 115 Q 405 99 429 109 L 429 135 Q 410 153 379 144 Z"/>
|
|
53
|
+
<path id="near-eye-white" fill="#ffffff" d="M 383 119 Q 405 108 423 113 L 423 132 Q 407 146 383 139 Z"/>
|
|
54
|
+
<path id="near-pupil" fill="#080d19" d="M 397 127 C 397 116 408 116 408 127 C 408 138 397 138 397 127 Z"/>
|
|
55
|
+
</g>
|
|
56
|
+
</svg>
|