Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 45 additions & 24 deletions scripts/sync-installer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,77 @@ import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = resolve(__dirname, '..');
const STATIC_DIR = resolve(ROOT_DIR, 'static');
const TARGET_FILE = resolve(STATIC_DIR, 'install.sh');

const LOCAL_CANONICAL_PATH = resolve(ROOT_DIR, '../wright/install.sh');
const REMOTE_CANONICAL_URL =
process.env.WRIGHT_INSTALLER_URL ||
'https://raw.githubusercontent.com/wrightkit/wright/main/install.sh';
const installers = [
{
name: 'install.sh',
targetFile: resolve(STATIC_DIR, 'install.sh'),
localPath: resolve(ROOT_DIR, '../wright/install.sh'),
pathEnv: 'WRIGHT_INSTALLER_PATH',
urlEnv: 'WRIGHT_INSTALLER_URL',
remoteUrl: 'https://raw.githubusercontent.com/wrightkit/wright/main/install.sh',
validate(content) {
if (!content.startsWith('#!/usr/bin/env bash') && !content.startsWith('#!/bin/bash')) {
throw new Error('install.sh must start with a bash shebang.');
}
}
},
{
name: 'install.ps1',
targetFile: resolve(STATIC_DIR, 'install.ps1'),
localPath: resolve(ROOT_DIR, '../wright/install.ps1'),
pathEnv: 'WRIGHT_WINDOWS_INSTALLER_PATH',
urlEnv: 'WRIGHT_WINDOWS_INSTALLER_URL',
remoteUrl: 'https://raw.githubusercontent.com/wrightkit/wright/main/install.ps1',
validate(content) {
if (!content.includes('[CmdletBinding()]') || !content.includes('x86_64-pc-windows-msvc')) {
throw new Error('install.ps1 is missing the expected PowerShell installer contract.');
}
}
}
];

async function getInstallerContent() {
const envPath = process.env.WRIGHT_INSTALLER_PATH;
async function getInstallerContent(installer) {
const envPath = process.env[installer.pathEnv];
if (envPath && existsSync(envPath)) {
return readFileSync(envPath, 'utf8');
}

if (existsSync(LOCAL_CANONICAL_PATH)) {
return readFileSync(LOCAL_CANONICAL_PATH, 'utf8');
if (existsSync(installer.localPath)) {
return readFileSync(installer.localPath, 'utf8');
}

const response = await fetch(REMOTE_CANONICAL_URL);
const remoteUrl = process.env[installer.urlEnv] || installer.remoteUrl;
const response = await fetch(remoteUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch installer from ${REMOTE_CANONICAL_URL}: ${response.status} ${response.statusText}`
);
throw new Error(`Failed to fetch ${installer.name} from ${remoteUrl}: ${response.status} ${response.statusText}`);
}
return await response.text();
}

function validateInstaller(content) {
function validateInstaller(installer, content) {
if (!content || typeof content !== 'string') {
throw new Error('Installer content is empty or invalid.');
}
if (!content.startsWith('#!/usr/bin/env bash') && !content.startsWith('#!/bin/bash')) {
throw new Error('Installer must start with bash shebang.');
throw new Error(`${installer.name} content is empty or invalid.`);
}
installer.validate(content);
if (!content.includes('wright') || !content.includes('wright-lsp')) {
throw new Error('Installer content missing expected binary definitions (wright / wright-lsp).');
throw new Error(`${installer.name} is missing expected binary definitions (wright / wright-lsp).`);
}
if (content.length < 500) {
throw new Error(`Installer content is suspiciously short (${content.length} bytes).`);
throw new Error(`${installer.name} is suspiciously short (${content.length} bytes).`);
}
}

async function main() {
const content = await getInstallerContent();
validateInstaller(content);

if (!existsSync(STATIC_DIR)) {
mkdirSync(STATIC_DIR, { recursive: true });
}

writeFileSync(TARGET_FILE, content, 'utf8');
for (const installer of installers) {
const content = await getInstallerContent(installer);
validateInstaller(installer, content);
writeFileSync(installer.targetFile, content, 'utf8');
}
}

main().catch((err) => {
Expand Down
46 changes: 33 additions & 13 deletions scripts/verify-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,39 @@ import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = resolve(__dirname, '..');
const BUILD_INSTALLER = resolve(ROOT_DIR, 'build/install.sh');

if (!existsSync(BUILD_INSTALLER)) {
console.error('[verify-build] Error: build/install.sh does not exist.');
process.exit(1);
}
const installers = [
{
name: 'install.sh',
path: resolve(ROOT_DIR, 'build/install.sh'),
validate(content) {
return content.startsWith('#!/usr/bin/env bash') || content.startsWith('#!/bin/bash');
},
error: 'build/install.sh has an invalid shebang.'
},
{
name: 'install.ps1',
path: resolve(ROOT_DIR, 'build/install.ps1'),
validate(content) {
return content.includes('[CmdletBinding()]') && content.includes('x86_64-pc-windows-msvc');
},
error: 'build/install.ps1 is missing the expected PowerShell installer contract.'
}
];

const content = readFileSync(BUILD_INSTALLER, 'utf8');
if (!content.startsWith('#!/usr/bin/env bash') && !content.startsWith('#!/bin/bash')) {
console.error('[verify-build] Error: build/install.sh has invalid shebang.');
process.exit(1);
}
if (!content.includes('wright') || !content.includes('wright-lsp')) {
console.error('[verify-build] Error: build/install.sh is missing wright binary references.');
process.exit(1);
for (const installer of installers) {
if (!existsSync(installer.path)) {
console.error(`[verify-build] Error: build/${installer.name} does not exist.`);
process.exit(1);
}

const content = readFileSync(installer.path, 'utf8');
if (!installer.validate(content)) {
console.error(`[verify-build] Error: ${installer.error}`);
process.exit(1);
}
if (!content.includes('wright') || !content.includes('wright-lsp')) {
console.error(`[verify-build] Error: build/${installer.name} is missing wright binary references.`);
process.exit(1);
}
}
17 changes: 16 additions & 1 deletion src/lib/components/InstallSection.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
<script lang="ts">
import { onMount } from 'svelte';
import CopyButton from './CopyButton.svelte';
import { install, site } from '$lib/site';
import { install } from '$lib/site';

let activeTargetId = $state('macos');
let activeTarget = $derived(
install.targets.find((t) => t.id === activeTargetId) ?? install.targets[0]
);

function detectInstallTarget() {
const userAgent = navigator.userAgent.toLowerCase();
if (/android|iphone|ipad|ipod/.test(userAgent)) return null;
if (userAgent.includes('windows')) return 'windows';
if (userAgent.includes('macintosh')) return 'macos';
if (userAgent.includes('linux') || userAgent.includes('x11')) return 'linux';
return null;
}

onMount(() => {
const detectedTarget = detectInstallTarget();
if (detectedTarget) activeTargetId = detectedTarget;
});
</script>

<section id="install" class="scroll-target border-t border-ink-800">
Expand Down
10 changes: 5 additions & 5 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,11 @@ export const install = {
id: 'windows',
label: 'Windows',
badge: 'x86_64',
method: 'GitHub Release archive',
command: 'https://github.com/wrightkit/wright/releases',
altMethod: 'See release documentation',
altCommand: 'docs/release.md',
note: 'Use the currently published Windows distribution path from the release documentation.'
method: 'PowerShell installer',
command: 'irm https://wrightkit.dev/install.ps1 | iex',
altMethod: 'Custom install directory',
altCommand: '$script = irm https://wrightkit.dev/install.ps1; & ([scriptblock]::Create($script)) -InstallDir "$HOME\\bin"',
note: 'Downloads the matching Windows release and verifies its checksum.'
},
{
id: 'ci',
Expand Down