Skip to content

Commit 9b28034

Browse files
ralyodioclaude
andauthored
feat: curl | sh installer for qc, shown on the homepage (#304)
public/install.sh installs @profullstack/qryptchat (qc) with bun if present, else npm on Node >= 22, falling back to ~/.local when the global npm prefix is not writable. The homepage hero shows the one-liner with a copy button. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f5f14a7 commit 9b28034

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

‎public/install.sh‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/sh
2+
# Install qc, the qrypt.chat terminal client (npm: @profullstack/qryptchat).
3+
#
4+
# curl -fsSL https://qrypt.chat/install.sh | sh
5+
#
6+
# Uses bun if present, otherwise npm (Node >= 22). When the global npm prefix
7+
# is not writable it installs into ~/.local instead of asking for sudo.
8+
set -eu
9+
10+
PKG="@profullstack/qryptchat"
11+
12+
say() { printf '%s\n' "$*"; }
13+
die() { printf 'qc install: %s\n' "$*" >&2; exit 1; }
14+
have() { command -v "$1" >/dev/null 2>&1; }
15+
16+
say "Installing qc (qrypt.chat terminal client)..."
17+
18+
if have bun; then
19+
bun add -g "$PKG"
20+
BIN_DIR="${BUN_INSTALL:-$HOME/.bun}/bin"
21+
elif have npm && have node; then
22+
major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)
23+
[ "$major" -ge 22 ] || die "Node >= 22 required (found $(node -v)). Upgrade Node or install bun: https://bun.sh"
24+
prefix=$(npm config get prefix)
25+
modules="$prefix/lib/node_modules"
26+
[ -d "$modules" ] || modules="$prefix/lib"
27+
if [ -w "$modules" ] && [ -w "$prefix/bin" ]; then
28+
npm install -g "$PKG"
29+
BIN_DIR="$prefix/bin"
30+
else
31+
BIN_DIR="$HOME/.local/bin"
32+
say "npm prefix $prefix is not writable; installing into $HOME/.local"
33+
npm install -g --prefix "$HOME/.local" "$PKG"
34+
fi
35+
else
36+
die "needs bun (https://bun.sh) or Node >= 22 with npm (https://nodejs.org)"
37+
fi
38+
39+
say ""
40+
if have qc; then
41+
say "$(qc --version 2>/dev/null || echo qc) installed."
42+
else
43+
say "qc installed to $BIN_DIR, which is not on your PATH. Add it:"
44+
say " export PATH=\"$BIN_DIR:\$PATH\""
45+
fi
46+
say ""
47+
say "Next:"
48+
say " qc login # approve in your browser; keys are sealed to this device"
49+
say " qc # open the TUI"
50+
say " qc --help # CLI and MCP usage"

‎src/app/home-content.jsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
'use client';
22

33
import Link from 'next/link';
4+
import { useState } from 'react';
45
import { useI18n } from '@/lib/hooks/useI18n.js';
56

7+
const INSTALL_CMD = 'curl -fsSL https://qrypt.chat/install.sh | sh';
8+
69
export default function HomeContent() {
710
const { t } = useI18n();
11+
const [copied, setCopied] = useState(false);
12+
13+
const copyInstall = async () => {
14+
try {
15+
await navigator.clipboard.writeText(INSTALL_CMD);
16+
setCopied(true);
17+
setTimeout(() => setCopied(false), 2000);
18+
} catch {
19+
// Clipboard blocked (insecure context or denied); the command stays selectable.
20+
}
21+
};
822

923
return (
1024
<>
@@ -19,6 +33,16 @@ export default function HomeContent() {
1933
<Link href="/auth" className="btn btn-primary">{t('nav.register')}</Link>
2034
<Link href="/auth" className="btn btn-secondary">{t('nav.login')}</Link>
2135
</div>
36+
<div className="hero-install">
37+
<span className="hero-install-label">Terminal client (qc): TUI, CLI and MCP</span>
38+
<div className="hero-install-cmd">
39+
<span className="hero-install-prompt" aria-hidden="true">$</span>
40+
<code>{INSTALL_CMD}</code>
41+
<button type="button" className="hero-install-copy" onClick={copyInstall} aria-label="Copy install command">
42+
{copied ? 'Copied' : 'Copy'}
43+
</button>
44+
</div>
45+
</div>
2246
</div>
2347
<div className="hero-visual">
2448
<img src="/hero.svg" alt="Illustration of a locked, end-to-end encrypted QryptChat conversation protected by post-quantum cryptography" className="hero-image" width="500" height="500" />
@@ -111,6 +135,13 @@ export default function HomeContent() {
111135
.hero-actions .btn-primary:hover { background-color: var(--color-bg-secondary); transform: translateY(-2px); box-shadow: 0 10px 25px rgba(0,0,0,.2); }
112136
.hero-actions .btn-secondary { background-color: transparent; color: white; border-color: rgba(255,255,255,.3); }
113137
.hero-actions .btn-secondary:hover { background-color: rgba(255,255,255,.1); border-color: white; }
138+
.hero-install { margin-top: var(--space-8); }
139+
.hero-install-label { display: block; font-size: .875rem; opacity: .8; margin-bottom: var(--space-2); }
140+
.hero-install-cmd { display: flex; align-items: center; gap: var(--space-3); max-width: 100%; background: rgba(0,0,0,.35); border: 1px solid rgba(255,255,255,.2); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); }
141+
.hero-install-cmd code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9375rem; color: #fff; text-align: left; user-select: all; }
142+
.hero-install-prompt { opacity: .6; user-select: none; }
143+
.hero-install-copy { flex-shrink: 0; background: transparent; color: #fff; border: 1px solid rgba(255,255,255,.35); border-radius: var(--radius-md); padding: var(--space-1) var(--space-3); font-size: .8125rem; font-weight: 600; cursor: pointer; }
144+
.hero-install-copy:hover { background: rgba(255,255,255,.1); border-color: #fff; }
114145
.hero-image { width: 100%; height: auto; max-width: 500px; opacity: 0.9; filter: drop-shadow(0 10px 30px rgba(0,0,0,.3)); }
115146
.features { padding: var(--space-20) 0; background-color: var(--color-bg-secondary); }
116147
.section-header { text-align: center; margin-bottom: var(--space-16); }

0 commit comments

Comments
 (0)