Skip to content
Open
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
134 changes: 134 additions & 0 deletions sitemd/0.1.2/sitemd/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/bin/sh
# sitemd install — downloads the sitemd binary for your platform.
# No Node.js required. Run: ./sitemd/install
#
# This script detects your OS and architecture, downloads the correct
# binary from GitHub Releases, and places it at sitemd/sitemd.

set -e

REPO="sitemd-cc/sitemd"
BINARY_NAME="sitemd"
FORCE=0
[ "$1" = "--force" ] && FORCE=1

# Detect platform
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

case "$OS" in
darwin) PLATFORM="darwin" ;;
linux) PLATFORM="linux" ;;
mingw*|msys*|cygwin*) PLATFORM="win" ;;
*) echo "Unsupported OS: $OS" >&2; exit 1 ;;
esac

case "$ARCH" in
x86_64|amd64) ARCH="x64" ;;
arm64|aarch64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
esac

if [ "$PLATFORM" = "win" ]; then
ASSET="sitemd-${PLATFORM}-${ARCH}.zip"
BINARY_NAME="sitemd.exe"
else
ASSET="sitemd-*-${PLATFORM}-${ARCH}.tar.gz"
fi

# Find the sitemd directory (where this script lives)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Determine wanted version. Prefer the sibling package.json (npm package or
# project created by `sitemd init`); fall back to GitHub Releases API for
# clone-from-source and download-the-tarball flows.
WANTED_VERSION=""
if [ -f "$SCRIPT_DIR/../package.json" ]; then
WANTED_VERSION=$(grep '"version"' "$SCRIPT_DIR/../package.json" | head -1 | sed 's/.*"version" *: *"\([^"]*\)".*/\1/')
fi

if [ -z "$WANTED_VERSION" ]; then
echo " sitemd install"
echo " Platform: ${PLATFORM}-${ARCH}"
echo " Finding latest release..."
if command -v curl >/dev/null 2>&1; then
LATEST=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
elif command -v wget >/dev/null 2>&1; then
LATEST=$(wget -qO- "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"//;s/".*//')
else
echo " Error: curl or wget required" >&2
exit 1
fi
if [ -z "$LATEST" ]; then
echo " Error: could not determine latest release" >&2
exit 1
fi
WANTED_VERSION="${LATEST#v}"
fi

VERSION="$WANTED_VERSION"
LATEST="v$VERSION"

# Idempotency: if a binary is already installed at the wanted version, exit.
if [ "$FORCE" = "0" ] && [ -f "$SCRIPT_DIR/$BINARY_NAME" ]; then
EXISTING_VERSION=$("$SCRIPT_DIR/$BINARY_NAME" --version 2>/dev/null | head -1 | sed 's/.*[^0-9]\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/')
if [ -n "$EXISTING_VERSION" ] && [ "$EXISTING_VERSION" = "$VERSION" ]; then
exit 0
fi
if [ -n "$EXISTING_VERSION" ] && [ "$EXISTING_VERSION" != "$VERSION" ]; then
echo " sitemd: upgrading $EXISTING_VERSION -> $VERSION"
fi
fi

echo " sitemd install"
echo " Platform: ${PLATFORM}-${ARCH}"
echo " Version: $VERSION"

# Build download URL
ARCHIVE="sitemd-${VERSION}-${PLATFORM}-${ARCH}"
if [ "$PLATFORM" = "win" ]; then
ARCHIVE="${ARCHIVE}.zip"
else
ARCHIVE="${ARCHIVE}.tar.gz"
fi
URL="https://github.com/$REPO/releases/download/$LATEST/$ARCHIVE"

# Download
TMPDIR=$(mktemp -d)
TMPFILE="$TMPDIR/$ARCHIVE"

echo " Downloading $ARCHIVE..."
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$TMPFILE" "$URL"
else
wget -q -O "$TMPFILE" "$URL"
fi

# Extract binary
echo " Extracting..."
if [ "$PLATFORM" = "win" ]; then
unzip -qo "$TMPFILE" -d "$TMPDIR/extracted"
else
mkdir -p "$TMPDIR/extracted"
tar -xzf "$TMPFILE" -C "$TMPDIR/extracted"
fi

# Find the binary inside the archive (it's at sitemd/sitemd inside the archive)
EXTRACTED_BIN=$(find "$TMPDIR/extracted" -name "$BINARY_NAME" -type f | head -1)
if [ -z "$EXTRACTED_BIN" ]; then
echo " Error: binary not found in archive" >&2
rm -rf "$TMPDIR"
exit 1
fi

# Install
cp "$EXTRACTED_BIN" "$SCRIPT_DIR/$BINARY_NAME"
chmod +x "$SCRIPT_DIR/$BINARY_NAME"

# Clean up
rm -rf "$TMPDIR"

echo ""
echo " Installed: $SCRIPT_DIR/$BINARY_NAME"
echo " Run: ./sitemd/sitemd launch"
echo ""
228 changes: 228 additions & 0 deletions sitemd/0.1.2/sitemd/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#!/usr/bin/env node
/**
* sitemd install — downloads the platform binary from GitHub Releases.
*
* Cross-platform Node bootstrap. Used as the npm postinstall hook and as a
* manual recovery command when --ignore-scripts was used. The shell script
* `install` next to this file does the same job for environments without Node.
*
* Idempotent: re-running with a matching binary version is a no-op.
*
* Source of truth for the wanted version is the sibling `../package.json`.
* That works in both contexts where this script ships:
* - npm package: ../package.json is the published @sitemd-cc/sitemd version
* - sitemd init project: ../package.json is the version copied at init time
*/

const fs = require('fs')
const path = require('path')
const os = require('os')
const https = require('https')
const { execFileSync } = require('child_process')

const GITHUB_RELEASE_URL = 'https://github.com/sitemd-cc/sitemd/releases/download'

const FORCE = process.argv.includes('--force')

function detectPlatform() {
const platform = process.platform === 'darwin' ? 'darwin'
: process.platform === 'linux' ? 'linux'
: process.platform === 'win32' ? 'win' : null
if (!platform) throw new Error(`Unsupported platform: ${process.platform}`)
const arch = process.arch === 'arm64' ? 'arm64' : 'x64'
const ext = platform === 'win' ? '.zip' : '.tar.gz'
const binaryName = platform === 'win' ? 'sitemd.exe' : 'sitemd'
return { platform, arch, ext, binaryName }
}

function readWantedVersion() {
const pkgPath = path.join(__dirname, '..', 'package.json')
if (!fs.existsSync(pkgPath)) return null
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
return pkg.version || null
} catch {
return null
}
}

function readExistingVersion(binaryPath) {
if (!fs.existsSync(binaryPath)) return null
try {
const out = execFileSync(binaryPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
const match = out.match(/(\d+\.\d+\.\d+)/)
return match ? match[1] : null
} catch {
return null
}
}

function download(url, dest) {
return new Promise((resolve, reject) => {
const follow = (u) => {
https.get(u, { headers: { 'User-Agent': 'sitemd-install' } }, res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return follow(res.headers.location)
}
if (res.statusCode !== 200) {
return reject(new Error(`Download failed: HTTP ${res.statusCode} ${u}`))
}
const file = fs.createWriteStream(dest)
res.pipe(file)
file.on('finish', () => { file.close(); resolve() })
file.on('error', reject)
}).on('error', reject)
}
follow(url)
})
}

// ---------------------------------------------------------------------------
// Agent file setup — places .mcp.json, CLAUDE.md, skills at project root
// Non-interactive: only creates files that don't exist, never overwrites.
// ---------------------------------------------------------------------------

function copyDirRecursive(src, dest) {
fs.mkdirSync(dest, { recursive: true })
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
if (entry.name === '.DS_Store' || entry.name.startsWith('._')) continue
const s = path.join(src, entry.name)
const d = path.join(dest, entry.name)
if (entry.isDirectory()) copyDirRecursive(s, d)
else fs.copyFileSync(s, d)
}
}

function setupAgentFiles(sitemdDir, projectRoot) {
const agentRes = path.join(sitemdDir, 'agent-resources')
if (!fs.existsSync(agentRes)) return

const binaryRel = './' + path.relative(projectRoot, path.join(sitemdDir, 'sitemd')).split(path.sep).join('/')

// .mcp.json — merge sitemd entry or create fresh
const mcpPath = path.join(projectRoot, '.mcp.json')
const mcpEntry = { sitemd: { command: binaryRel, args: ['mcp'] } }
if (fs.existsSync(mcpPath)) {
try {
const existing = JSON.parse(fs.readFileSync(mcpPath, 'utf8'))
if (!existing.mcpServers?.sitemd) {
existing.mcpServers = { ...existing.mcpServers, ...mcpEntry }
fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n')
console.log(' sitemd: added server entry to .mcp.json')
}
} catch {} // malformed — leave it alone
} else {
fs.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mcpEntry }, null, 2) + '\n')
console.log(' sitemd: created .mcp.json')
}

// CLAUDE.md, AGENTS.md — only if missing
for (const file of ['CLAUDE.md', 'AGENTS.md']) {
const src = path.join(agentRes, file)
const dest = path.join(projectRoot, file)
if (fs.existsSync(src) && !fs.existsSync(dest)) {
fs.copyFileSync(src, dest)
console.log(` sitemd: created ${file}`)
}
}

// .claude/skills/, .agents/skills/ — only if target dir missing
for (const rel of [path.join('.claude', 'skills'), path.join('.agents', 'skills')]) {
const srcDir = path.join(agentRes, rel)
const destDir = path.join(projectRoot, rel)
if (fs.existsSync(srcDir) && !fs.existsSync(destDir)) {
copyDirRecursive(srcDir, destDir)
console.log(` sitemd: created ${rel.split(path.sep).join('/')}/`)
}
}
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
const { platform, arch, ext, binaryName } = detectPlatform()
const binaryPath = path.join(__dirname, binaryName)
const wanted = readWantedVersion()

if (!wanted) {
console.error(` sitemd install: could not read version from ../package.json`)
console.error(` Try the shell installer instead: ./sitemd/install`)
return
}

// Idempotency: skip if existing binary matches wanted version
if (!FORCE) {
const existing = readExistingVersion(binaryPath)
if (existing && existing === wanted) {
// Silent no-op — common case for re-runs
return
}
if (existing && existing !== wanted) {
console.log(` sitemd: upgrading ${existing} → ${wanted}`)
}
}

const archive = `sitemd-${wanted}-${platform}-${arch}${ext}`
const url = `${GITHUB_RELEASE_URL}/v${wanted}/${archive}`
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sitemd-install-'))
const archivePath = path.join(tmpDir, archive)
const extractDir = path.join(tmpDir, 'extracted')

try {
console.log(` sitemd: downloading v${wanted} (${platform}-${arch})...`)
await download(url, archivePath)

fs.mkdirSync(extractDir, { recursive: true })
if (ext === '.zip') {
// Windows 10 1803+ ships tar.exe which handles zip files
execFileSync('tar', ['-xf', archivePath, '-C', extractDir], { stdio: 'ignore' })
} else {
execFileSync('tar', ['-xzf', archivePath, '-C', extractDir], { stdio: 'ignore' })
}

// Find the binary inside the extracted archive (under sitemd/)
let sourceRoot = extractDir
if (fs.existsSync(path.join(extractDir, 'sitemd'))) {
sourceRoot = path.join(extractDir, 'sitemd')
} else {
const entries = fs.readdirSync(extractDir)
if (entries.length === 1 && fs.statSync(path.join(extractDir, entries[0])).isDirectory()) {
sourceRoot = path.join(extractDir, entries[0])
if (fs.existsSync(path.join(sourceRoot, 'sitemd'))) {
sourceRoot = path.join(sourceRoot, 'sitemd')
}
}
}
const extractedBinary = path.join(sourceRoot, binaryName)
if (!fs.existsSync(extractedBinary)) {
throw new Error(`Binary not found in archive: ${binaryName}`)
}

fs.rmSync(binaryPath, { force: true })
fs.copyFileSync(extractedBinary, binaryPath)
if (platform !== 'win') fs.chmodSync(binaryPath, 0o755)

console.log(` sitemd v${wanted} installed`)

// Propagate agent files to the project root (where npm install was invoked).
// INIT_CWD is set by npm to the directory where `npm install` was run.
const projectRoot = process.env.INIT_CWD
if (projectRoot && path.resolve(projectRoot) !== path.resolve(__dirname, '..')) {
setupAgentFiles(__dirname, projectRoot)
}
} catch (err) {
console.error(` sitemd install failed: ${err.message}`)
console.error(` To retry: node sitemd/install.js (or ./sitemd/install on Unix)`)
} finally {
try { fs.rmSync(tmpDir, { recursive: true, force: true }) } catch {}
}
}

main().catch(err => {
console.error(` sitemd install failed: ${err.message}`)
console.error(` To retry: node sitemd/install.js (or ./sitemd/install on Unix)`)
// Always exit 0 — npm install must not fail because of binary download trouble
process.exit(0)
})
Loading