diff --git a/sitemd/0.1.2/sitemd/install b/sitemd/0.1.2/sitemd/install new file mode 100755 index 00000000..d741a9fb --- /dev/null +++ b/sitemd/0.1.2/sitemd/install @@ -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 "" diff --git a/sitemd/0.1.2/sitemd/install.js b/sitemd/0.1.2/sitemd/install.js new file mode 100644 index 00000000..e7faf842 --- /dev/null +++ b/sitemd/0.1.2/sitemd/install.js @@ -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) +}) diff --git a/sitemd/0.1.2/skills/SKILL.md b/sitemd/0.1.2/skills/SKILL.md new file mode 100644 index 00000000..8abbbc5b --- /dev/null +++ b/sitemd/0.1.2/skills/SKILL.md @@ -0,0 +1,74 @@ +--- +name: sitemd +description: Build and manage sitemd static websites from Markdown. Create pages, generate content, configure settings, and deploy. +--- + +# sitemd + +You are working in a sitemd project — a markdown-based static site builder with MCP integration. + +## Project Structure + +- `sitemd` — Compiled binary (run `./sitemd/sitemd launch`) +- `install` — Bootstrap script (run `./sitemd/install` to download binary) +- `install.js` — Cross-platform Node bootstrap (run `node sitemd/install.js`); also runs automatically as the npm `postinstall` hook +- `pages/` — Markdown content files with YAML frontmatter +- `settings/` — Site configuration (YAML frontmatter in `.md` files) +- `theme/` — CSS and HTML templates +- `media/` — Images and assets +- `site/` — Built output + +## Available MCP Tools + +Use these tools to manage the site: + +| Tool | Purpose | +|---|---| +| `sitemd_status` | Project state overview | +| `sitemd_pages_create` | Create new pages (writes file + nav + groups) | +| `sitemd_pages_create_batch` | Create multiple pages in one call | +| `sitemd_pages_delete` | Delete a page (cleans up nav + groups) | +| `sitemd_groups_add_pages` | Add pages to group sidebar | +| `sitemd_site_context` | Site identity, pages, conventions | +| `sitemd_content_validate` | Validate content | +| `sitemd_seo_audit` | SEO health check with scored report | +| `sitemd_init` | Initialize project from template | +| `sitemd_build` | Build site locally | +| `sitemd_deploy` | Build and deploy site | +| `sitemd_activate` | Activate site (permanent) | +| `sitemd_clone` | Clone existing website | +| `sitemd_config_set` | Set backend config | +| `sitemd_update_check` | Check for updates | + +Read pages, settings, and groups files directly — no MCP tool needed for reads. + +## First Steps + +1. **If no binary** (`sitemd/sitemd` does not exist) — run `./sitemd/install` (Unix) or `node sitemd/install.js` (any platform) to download it. The MCP server runs the binary; without it, every sitemd_* tool call will fail. +2. Call `sitemd_status` to understand the project state +3. Read files in `pages/` to see existing content +4. Call `sitemd_site_context` with a content type to get site identity, conventions, and existing pages +5. Create pages with `sitemd_pages_create` — use rich components (buttons, cards, embeds, galleries) +6. Validate with `sitemd_content_validate` + +## Settings + +All configuration lives in `settings/*.md` frontmatter. Key files: `meta.md` (identity), `header.md` (nav), `footer.md` (footer), `groups.md` (page groups), `theme.md` (colors/fonts), `build.md` (dev server), `deploy.md` (deployment). + +## Markdown Extensions + +Beyond standard markdown, sitemd supports rich components. The syntax reference is below. + +- `button: Label: /slug` — styled buttons. Modifiers: `+outline`, `+big`, `+newtab`, `+color:red` +- `card: Title` / `card-text:` / `card-image:` / `card-link:` — responsive card grids +- `embed: URL` — auto-detects YouTube, Vimeo, Spotify, X, CodePen, etc. +- `gallery:` with indented `![alt](url)` — image grid with lightbox +- `image-row:` with indented `![alt](url)` — equal-height image row +- `![alt](url +width:N +circle +bw +expand)` — image modifiers +- `[text]{tooltip content}` — inline tooltips +- `modal: id` with indented content, trigger via `[link](#modal:id)` — modal dialogs +- `{#custom-id}` — inline anchors +- `[text](url+newtab)` — link modifiers +- `form:` with indented YAML — forms +- `gated: type1, type2` ... `/gated` — gated sections +- `data: source` / `data-display: cards|list|table` — dynamic data \ No newline at end of file