diff --git a/.github/workflows/publish-manifest.yml b/.github/workflows/publish-manifest.yml deleted file mode 100644 index 268e2ec6..00000000 --- a/.github/workflows/publish-manifest.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: publish-manifest - -# Receives a `repository_dispatch` event of type `manifest-update` from -# `web4/.github/workflows/release.yml` after a tag ships. The event carries -# the rendered manifest in `client_payload.manifest`; this workflow merges it -# into `public/.well-known/latest.json` and commits to main, which triggers -# the Cloudflare Pages deploy. -# -# Merge rules: -# - `latest_stable` is only advanced when the incoming manifest is for a -# non-prerelease tag (`is_prerelease == false`). RC tags update only -# `latest_prerelease` and `channels.edge`. -# - `platforms` is replaced wholesale with the incoming map for the tag. -# - `updated_at` is taken from the incoming manifest. -# -# Manual trigger (`workflow_dispatch`) is available for the case where the -# dispatch from web4 was dropped (missing token, transient 5xx) and the -# operator needs to nudge the manifest forward by hand. - -on: - repository_dispatch: - types: [manifest-update] - workflow_dispatch: - inputs: - manifest: - description: 'Inline manifest JSON (overrides the dispatch payload path)' - required: true - -permissions: - contents: write - -jobs: - merge-and-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Resolve incoming manifest - id: incoming - env: - DISPATCH_MANIFEST: ${{ toJson(github.event.client_payload.manifest) }} - MANUAL_MANIFEST: ${{ inputs.manifest }} - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "$MANUAL_MANIFEST" > /tmp/incoming.json - else - echo "$DISPATCH_MANIFEST" > /tmp/incoming.json - fi - jq . /tmp/incoming.json > /tmp/incoming.pretty.json - cp /tmp/incoming.pretty.json /tmp/incoming.json - echo "incoming manifest:" - cat /tmp/incoming.json - - - name: Merge into public/.well-known/latest.json - run: | - INCOMING=/tmp/incoming.json - CURRENT=public/.well-known/latest.json - mkdir -p public/.well-known - if [ ! -f "$CURRENT" ]; then - # First-ever publish — start from the incoming manifest, but - # rewrite the prerelease-aware fields into their final names. - jq '{ - "$schema": "https://pilotprotocol.network/.well-known/latest.schema.json", - "updated_at": .updated_at, - "latest_stable": (.proposed_stable // ""), - "latest_prerelease": (.proposed_edge // .tag), - "channels": { - "stable": (.proposed_stable // ""), - "edge": (.proposed_edge // .tag) - }, - "release_notes_url": .release_notes_url, - "homebrew_formula_url": .homebrew_formula_url, - "platforms": .platforms - }' "$INCOMING" > "$CURRENT" - else - jq --slurpfile inc "$INCOMING" ' - . as $cur | - $inc[0] as $i | - { - "$schema": ($cur["$schema"] // "https://pilotprotocol.network/.well-known/latest.schema.json"), - "updated_at": $i.updated_at, - "latest_stable": (if $i.is_prerelease then $cur.latest_stable else $i.proposed_stable end), - "latest_prerelease": ($i.proposed_edge // $i.tag), - "channels": { - "stable": (if $i.is_prerelease then $cur.channels.stable else $i.proposed_stable end), - "edge": ($i.proposed_edge // $i.tag) - }, - "release_notes_url": $i.release_notes_url, - "homebrew_formula_url": $i.homebrew_formula_url, - "platforms": $i.platforms - } - ' "$CURRENT" > "$CURRENT.new" - mv "$CURRENT.new" "$CURRENT" - fi - echo "merged manifest:" - cat "$CURRENT" - - - name: Commit and push - run: | - if git diff --quiet public/.well-known/latest.json; then - echo "manifest unchanged — nothing to commit." - exit 0 - fi - git config user.name "pilot-release-bot" - git config user.email "release-bot@pilotprotocol.network" - TAG=$(jq -r '.latest_prerelease // .latest_stable // "unknown"' public/.well-known/latest.json) - git add public/.well-known/latest.json - git commit -m "manifest: bump to ${TAG}" \ - -m "Triggered by upstream release workflow." - git push origin main diff --git a/.github/workflows/sync-install-sh.yml b/.github/workflows/sync-install-sh.yml new file mode 100644 index 00000000..a2ebf3f3 --- /dev/null +++ b/.github/workflows/sync-install-sh.yml @@ -0,0 +1,75 @@ +name: sync-install-sh + +# Keep the live https://pilotprotocol.network/install.sh in sync with the +# canonical script. The pilot-release Worker serves /install.sh from the +# pilot-release-assets R2 bucket — NOT from Pages — so a git-side fix that +# never reaches R2 is invisible to users. That exact gap shipped a stale, +# broken installer for ~6 weeks (fresh installs aborted at +# "cp: pilot-gateway: No such file or directory", reported 2026-07-06). +# +# Source of truth: pilot-protocol/release:install.sh (main). +# This repo's public/install.sh is a shadowed fallback kept byte-identical. + +on: + push: + branches: [main] + paths: + - public/install.sh + - .github/workflows/sync-install-sh.yml + schedule: + - cron: '17 6 * * *' # daily — catches changes that land only in pilot-protocol/release + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: sync-install-sh + cancel-in-progress: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Fetch canonical install.sh from pilot-protocol/release + run: | + curl -fsSL "https://raw.githubusercontent.com/pilot-protocol/release/main/install.sh" -o canonical-install.sh + bash -n canonical-install.sh + # The installer must never hard-depend on the gateway binary — + # release tarballs ship daemon/pilotctl/updater only. Guard the + # regression that broke fresh installs. + if grep -qE '^\s*cp "\$TMPDIR/pilot-gateway"' canonical-install.sh; then + if ! grep -qE 'elif \[ -f "\$TMPDIR/pilot-gateway" \]' canonical-install.sh; then + echo "::error::canonical install.sh copies pilot-gateway unconditionally — refusing to publish a script that aborts fresh installs" + exit 1 + fi + fi + + - name: Warn when the Pages fallback copy has drifted + run: | + if ! diff -q canonical-install.sh public/install.sh >/dev/null; then + echo "::warning::public/install.sh differs from pilot-protocol/release:install.sh — the live copy (R2) follows the release repo; update public/install.sh to match" + fi + + - name: Upload to R2 (served by the pilot-release Worker) + env: + # CLOUDFLARE_API_TOKEN (Pages-scoped) returned 403 code 10000 on the + # R2 objects endpoint — mint a token with Account → R2 → Edit scope + # and store it as CLOUDFLARE_R2_TOKEN. Falls back to the Pages token + # so the day that one gains R2 scope this also starts working. + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_R2_TOKEN || secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + npx --yes wrangler@4 r2 object put pilot-release-assets/install.sh \ + --file canonical-install.sh \ + --content-type "text/plain; charset=utf-8" \ + --remote + + - name: Verify the live URL serves the canonical script + run: | + sleep 5 + curl -fsSL -H 'Cache-Control: no-cache' "https://pilotprotocol.network/install.sh" -o live-install.sh + diff canonical-install.sh live-install.sh + echo "live install.sh matches canonical" diff --git a/.gitignore b/.gitignore index cbd3fcfe..f87d2c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ worker/coverage/ .env .gstack/ + +# macOS +.DS_Store diff --git a/.snippets/blog-bar.md b/.snippets/blog-bar.md new file mode 100644 index 00000000..69c9e2cf --- /dev/null +++ b/.snippets/blog-bar.md @@ -0,0 +1,144 @@ +# Latest-blog bar (saved for later re-insertion) + +A slim, angular, theme-flipping (white-on-dark / black-on-light) outlined strip with +an outer glow, showing the 3 most recent blog posts (thumbnail + date + title) below +the thesis section's "One line of code gets an agent online. No SDK. No API key." + +Removed from `src/pages/index.astro` on 2026-07-07 at the user's request; restore all +three pieces below verbatim when asked to "put the blog bar back". + +--- + +## 1. Frontmatter (top of `src/pages/index.astro`, after the `getLiveStats` import + call) + +Add the import: +```js +import { blogPosts } from '../data/blogPosts'; +``` + +Add after `const { liveAgents, liveAgentsExact, liveRequests } = await getLiveStats();`: +```js +// Three most recent posts for the thesis blog bar, newest first. +const latestPosts = [...blogPosts] + .sort((a, b) => (b.iso_date ?? '').localeCompare(a.iso_date ?? '')) + .slice(0, 3); +``` + +## 2. Markup — inside `#thesis`, immediately after the closing `` of the +`.thesis` block (i.e. right after the "One line of code…" paragraph's container), +before the section closes: + +```html + +
+
+ From the blog + All posts → +
+
+ {latestPosts.map((post) => ( + + + + + + {post.date} + {post.title} + + + ))} +
+
+``` + +## 3. CSS — inside the homepage ` + activateFromHash(); + window.addEventListener('hashchange', activateFromHash); + + // ---- ⌘K / focus ---- + const isMac = navigator.platform.toLowerCase().includes('mac'); + const hint = $('#kbd-hint'); + if (hint) hint.textContent = isMac ? '⌘K' : 'Ctrl K'; + window.addEventListener('keydown', (e) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); q.focus(); q.select(); } + if (e.key === 'Escape' && document.activeElement === q) { q.value = ''; applyFilter(); q.blur(); } + }); + diff --git a/src/pages/apps/[id].astro b/src/pages/apps/[id].astro new file mode 100644 index 00000000..f908a803 --- /dev/null +++ b/src/pages/apps/[id].astro @@ -0,0 +1,334 @@ +--- +import BaseHead from '../../components/BaseHead.astro'; +import Nav from '../../components/Nav.astro'; +import Footer from '../../components/Footer.astro'; +import AppIcon from '../../components/AppIcon.astro'; +import Shelf from '../../components/Shelf.astro'; +import { apps, categories, relatedApps } from '../../data/apps'; +import { fmtBytes, fmtDate, platformLabel, osOf, osSummary, ALL_PLATFORMS } from '../../data/format'; +import '../../styles/system.css'; +import '../../styles/appstore.css'; + +export function getStaticPaths() { + return apps.map((app) => ({ params: { id: app.id }, props: { app } })); +} +const { app } = Astro.props; + +const catName = (id) => categories.find((c) => c.id === id)?.name ?? id; +const related = relatedApps(app); +const installCmd = `pilotctl appstore install ${app.id}`; +const oses = [...new Set(app.bundles.map((b) => osOf(b.platform)))]; +const bundleFor = (p) => app.bundles.find((b) => b.platform === p); + +// tiny inline markdown: escape, then **bold**, `code`, and [text](url) +function inline(s) { + const esc = s.replace(/&/g, '&').replace(//g, '>'); + return esc + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/`([^`]+?)`/g, '$1') + .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s"'<>]+)\)/g, '$1'); +} +let descBlocks = app.description.split('\n\n').map((b) => b.trim()).filter(Boolean); +// drop a leading level-1 header (redundant with the page title) +if (descBlocks.length && /^#\s+/.test(descBlocks[0])) descBlocks = descBlocks.slice(1); +const paragraphs = descBlocks.map((p) => { + const lines = p.split('\n'); + const h = /^(#{1,6})\s+(.*)$/.exec(lines[0]); + if (lines.length === 1 && h) return { type: 'h', html: inline(h[2]) }; + const isList = lines.every((l) => /^\s*[-*]\s+/.test(l)); + if (isList) return { type: 'ul', items: lines.map((l) => inline(l.replace(/^\s*[-*]\s+/, ''))) }; + return { type: 'p', html: inline(p.replace(/\n/g, ' ')) }; +}); + +const title = `${app.name} — Pilot Protocol App Store`; +const description = app.tagline; +const canonicalUrl = `https://pilotprotocol.network/apps/${app.id}`; +--- + + + + + + + +