mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
Update here.now skill bundle
Made-with: Cursor
This commit is contained in:
parent
f7dfd4ae36
commit
21cc9c8d32
3 changed files with 521 additions and 17 deletions
|
|
@ -1,14 +1,17 @@
|
|||
---
|
||||
name: here-now
|
||||
name: here.now
|
||||
description: >
|
||||
Publish files and folders to the web instantly. Static hosting for HTML sites,
|
||||
images, PDFs, and any file type. Sites can connect to external APIs (LLMs,
|
||||
databases, email, payments) via proxy routes with server-side credential
|
||||
injection. Use when asked to "publish this", "host this", "deploy this",
|
||||
"share this on the web", "make a website", "put this online", "upload to
|
||||
the web", "create a webpage", "share a link", "serve this site", "generate
|
||||
a URL", or "build a chatbot". Outputs a live, shareable URL at {slug}.here.now.
|
||||
version: 1.14.0
|
||||
here.now lets agents publish websites and store private files in cloud
|
||||
Drives. Use Sites to publish HTML, documents, images, PDFs, videos, and
|
||||
static files to live URLs at {slug}.here.now or custom domains. Use Drives as private cloud
|
||||
folders where agents can store files (documents, context, memory, plans,
|
||||
assets, media, research, code, etc), share them with other agents, and
|
||||
continue across sessions and tools. Use when asked to "publish this", "host
|
||||
this", "deploy this", "share this on the web", "make a website", "put this
|
||||
online", "create a webpage", "generate a URL", "build a chatbot", "save this
|
||||
to my Drive", "store this for later", "write this to cloud storage", "share a
|
||||
folder with another agent", or "use my here.now Drive".
|
||||
version: 1.15.3
|
||||
author: here.now
|
||||
license: MIT
|
||||
prerequisites:
|
||||
|
|
@ -16,14 +19,19 @@ prerequisites:
|
|||
platforms: [macos, linux]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [here.now, herenow, publish, deploy, hosting, static-site, web, share, URL]
|
||||
tags: [here.now, herenow, publish, deploy, hosting, static-site, web, share, URL, drive, storage]
|
||||
homepage: https://here.now
|
||||
requires_toolsets: [terminal]
|
||||
---
|
||||
|
||||
# here.now
|
||||
|
||||
Create a live URL from any file or folder. Static hosting with optional proxy routes for calling external APIs server-side.
|
||||
here.now lets agents publish websites and store private files in cloud Drives.
|
||||
|
||||
Use here.now for two jobs:
|
||||
|
||||
- **Sites**: publish websites and files at `{slug}.here.now`.
|
||||
- **Drives**: store private agent files in cloud folders.
|
||||
|
||||
## Current docs
|
||||
|
||||
|
|
@ -40,6 +48,7 @@ Read the docs:
|
|||
|
||||
Topics that require current docs (do not rely on local skill text alone):
|
||||
|
||||
- Drives and Drive sharing
|
||||
- custom domains
|
||||
- payments and payment gating
|
||||
- forking
|
||||
|
|
@ -58,8 +67,11 @@ If the docs fetch fails or times out, continue with the local skill and live API
|
|||
|
||||
- Required binaries: `curl`, `file`, `jq`
|
||||
- Optional environment variable: `$HERENOW_API_KEY`
|
||||
- Optional Drive token variable: `$HERENOW_DRIVE_TOKEN`
|
||||
- Optional credentials file: `~/.herenow/credentials`
|
||||
- Skill script path: `${HERMES_SKILL_DIR}/scripts/publish.sh`
|
||||
- Skill helper paths:
|
||||
- `${HERMES_SKILL_DIR}/scripts/publish.sh` for publishing sites
|
||||
- `${HERMES_SKILL_DIR}/scripts/drive.sh` for private Drive storage
|
||||
|
||||
## Create a site
|
||||
|
||||
|
|
@ -90,6 +102,23 @@ The script auto-loads the `claimToken` from `.herenow/state.json` when updating
|
|||
|
||||
Authenticated updates require a saved API key.
|
||||
|
||||
## Use a Drive
|
||||
|
||||
Use a Drive when the user wants private cloud storage for agent files: documents, context, memory, plans, assets, media, research, code, and anything else that should persist without being published as a website.
|
||||
|
||||
Every signed-in account has a default Drive named `My Drive`.
|
||||
|
||||
```bash
|
||||
DRIVE="${HERMES_SKILL_DIR}/scripts/drive.sh"
|
||||
bash "$DRIVE" default
|
||||
bash "$DRIVE" ls "My Drive"
|
||||
bash "$DRIVE" put "My Drive" notes/today.md --from ./notes/today.md
|
||||
bash "$DRIVE" cat "My Drive" notes/today.md
|
||||
bash "$DRIVE" share "My Drive" --perms write --prefix notes/ --ttl 7d
|
||||
```
|
||||
|
||||
Use scoped Drive tokens for agent-to-agent handoff. If you receive a `herenow_drive` share block, use its `token` as `Authorization: Bearer <token>` against `api_base`, respect `pathPrefix` when present, and preserve ETags on writes. A `pathPrefix` of `null` means full-Drive access. If the skill is available, prefer `drive.sh`; otherwise call the listed API operations directly.
|
||||
|
||||
## API key storage
|
||||
|
||||
The publish script reads the API key from these sources (first match wins):
|
||||
|
|
@ -159,13 +188,21 @@ Never present this local file path as a URL, and never use it as source of truth
|
|||
|
||||
## What to tell the user
|
||||
|
||||
For published sites:
|
||||
|
||||
- Always share the `siteUrl` from the current script run.
|
||||
- Read and follow `publish_result.*` lines from script stderr to determine auth mode.
|
||||
- When `publish_result.auth_mode=authenticated`: tell the user the site is **permanent** and saved to their account. No claim URL is needed.
|
||||
- When `publish_result.auth_mode=anonymous`: tell the user the site **expires in 24 hours**. Share the claim URL (if `publish_result.claim_url` is non-empty and starts with `https://`) so they can keep it permanently. Warn that claim tokens are only returned once and cannot be recovered.
|
||||
- Never tell the user to inspect `.herenow/state.json` for claim URLs or auth status.
|
||||
|
||||
## Script options
|
||||
For Drives:
|
||||
|
||||
- Do not describe Drive files as public URLs.
|
||||
- Tell the user Drive contents are private unless shared with a scoped token.
|
||||
- When sharing access with another agent, prefer a scoped token with a narrow `pathPrefix` and short TTL.
|
||||
|
||||
## publish.sh options
|
||||
|
||||
| Flag | Description |
|
||||
| ---------------------- | -------------------------------------------- |
|
||||
|
|
@ -181,9 +218,9 @@ Never present this local file path as a URL, and never use it as source of truth
|
|||
| `--spa` | Enable SPA routing (serve index.html for unknown paths) |
|
||||
| `--forkable` | Allow others to fork this site |
|
||||
|
||||
## Beyond the script
|
||||
## Beyond publish.sh
|
||||
|
||||
For all other operations — delete, metadata, passwords, payments, domains, handles, links, variables, proxy routes, forking, duplication, and more — see the current docs:
|
||||
For Drive operations, use `drive.sh` or the Drive API. For broader account and site management — delete, metadata, passwords, payments, domains, handles, links, variables, proxy routes, forking, duplication, and more — see the current docs:
|
||||
|
||||
→ **https://here.now/docs**
|
||||
|
||||
|
|
|
|||
406
skills/productivity/here-now/scripts/drive.sh
Executable file
406
skills/productivity/here-now/scripts/drive.sh
Executable file
|
|
@ -0,0 +1,406 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="https://here.now"
|
||||
CREDENTIALS_FILE="$HOME/.herenow/credentials"
|
||||
API_KEY="${HERENOW_API_KEY:-}"
|
||||
DRIVE_TOKEN="${HERENOW_DRIVE_TOKEN:-}"
|
||||
ALLOW_NON_HERENOW_BASE_URL=0
|
||||
MAX_FILE_BYTES=$((500 * 1024 * 1024))
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: drive.sh [global options] <command> [args]
|
||||
|
||||
Global options:
|
||||
--api-key <key> Account API key (or $HERENOW_API_KEY / ~/.herenow/credentials)
|
||||
--token <drv_live_...> Drive token (or $HERENOW_DRIVE_TOKEN)
|
||||
--base-url <url> API base (default: https://here.now)
|
||||
--allow-nonherenow-base-url
|
||||
|
||||
Commands:
|
||||
create [name] [--default]
|
||||
default
|
||||
ls
|
||||
ls <drive> [prefix]
|
||||
cat <drive> <path>
|
||||
put <drive> <path> --from <local-file>
|
||||
import <drive> <prefix> --from <local-folder> [--dry-run]
|
||||
export <drive> <prefix> --to <local-folder> [--dry-run]
|
||||
rm <drive> <path> [--recursive --confirm <path>]
|
||||
share <drive> --perms read|write [--prefix notes/] [--ttl 30d] [--label text] [--manage-tokens]
|
||||
tokens <drive>
|
||||
revoke <drive> <tokenId>
|
||||
delete <drive> --confirm "<drive name>"
|
||||
USAGE
|
||||
exit 1
|
||||
}
|
||||
|
||||
die() { echo "error: $1" >&2; exit 1; }
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BUNDLED_JQ="${SKILL_DIR}/bin/jq"
|
||||
|
||||
if [[ -x "$BUNDLED_JQ" ]]; then
|
||||
JQ_BIN="$BUNDLED_JQ"
|
||||
elif command -v jq >/dev/null 2>&1; then
|
||||
JQ_BIN="$(command -v jq)"
|
||||
else
|
||||
die "requires jq"
|
||||
fi
|
||||
|
||||
for cmd in curl file; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || die "requires $cmd"
|
||||
done
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--api-key) API_KEY="$2"; shift 2 ;;
|
||||
--token) DRIVE_TOKEN="$2"; shift 2 ;;
|
||||
--base-url) BASE_URL="$2"; shift 2 ;;
|
||||
--allow-nonherenow-base-url) ALLOW_NON_HERENOW_BASE_URL=1; shift ;;
|
||||
--help|-h) usage ;;
|
||||
--*) die "unknown global option: $1" ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
CMD="${1:-}"
|
||||
[[ -n "$CMD" ]] || usage
|
||||
shift || true
|
||||
|
||||
if [[ -z "$API_KEY" && -z "$DRIVE_TOKEN" && -f "$CREDENTIALS_FILE" ]]; then
|
||||
API_KEY=$(tr -d '[:space:]' < "$CREDENTIALS_FILE")
|
||||
fi
|
||||
|
||||
BASE_URL="${BASE_URL%/}"
|
||||
if [[ "$BASE_URL" != "https://here.now" && "$ALLOW_NON_HERENOW_BASE_URL" -ne 1 ]]; then
|
||||
if [[ -n "$API_KEY" || -n "$DRIVE_TOKEN" ]]; then
|
||||
die "refusing to send credentials to non-default base URL; pass --allow-nonherenow-base-url to override"
|
||||
fi
|
||||
fi
|
||||
|
||||
auth_header=()
|
||||
if [[ -n "$DRIVE_TOKEN" ]]; then
|
||||
auth_header=(-H "authorization: Bearer $DRIVE_TOKEN")
|
||||
elif [[ -n "$API_KEY" ]]; then
|
||||
auth_header=(-H "authorization: Bearer $API_KEY")
|
||||
else
|
||||
die "missing credentials; set HERENOW_API_KEY, HERENOW_DRIVE_TOKEN, or ~/.herenow/credentials"
|
||||
fi
|
||||
|
||||
compute_sha256() {
|
||||
local f="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$f" | cut -d' ' -f1
|
||||
else
|
||||
shasum -a 256 "$f" | cut -d' ' -f1
|
||||
fi
|
||||
}
|
||||
|
||||
guess_content_type() {
|
||||
local f="$1"
|
||||
case "${f##*.}" in
|
||||
html|htm) echo "text/html; charset=utf-8" ;;
|
||||
css) echo "text/css; charset=utf-8" ;;
|
||||
js|mjs) echo "text/javascript; charset=utf-8" ;;
|
||||
json) echo "application/json; charset=utf-8" ;;
|
||||
md|txt) echo "text/plain; charset=utf-8" ;;
|
||||
svg) echo "image/svg+xml" ;;
|
||||
png) echo "image/png" ;;
|
||||
jpg|jpeg) echo "image/jpeg" ;;
|
||||
gif) echo "image/gif" ;;
|
||||
webp) echo "image/webp" ;;
|
||||
pdf) echo "application/pdf" ;;
|
||||
*) file --brief --mime-type "$f" 2>/dev/null || echo "application/octet-stream" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
api_json() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
local body="${1:-}"
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
local code
|
||||
if [[ -n "$body" ]]; then
|
||||
code=$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "$url" "${auth_header[@]}" -H "content-type: application/json" -d "$body")
|
||||
else
|
||||
code=$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" "$url" "${auth_header[@]}")
|
||||
fi
|
||||
if [[ "$code" -lt 200 || "$code" -ge 300 ]]; then
|
||||
local err
|
||||
err=$("$JQ_BIN" -r '.error // empty' "$tmp" 2>/dev/null || true)
|
||||
[[ -n "$err" ]] || err="$(cat "$tmp")"
|
||||
rm -f "$tmp"
|
||||
die "HTTP $code: $err"
|
||||
fi
|
||||
cat "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
urlenc() {
|
||||
"$JQ_BIN" -nr --arg v "$1" '$v|@uri'
|
||||
}
|
||||
|
||||
urlenc_path() {
|
||||
local path="$1"
|
||||
local out=""
|
||||
local part
|
||||
IFS='/' read -r -a parts <<< "$path"
|
||||
for part in "${parts[@]}"; do
|
||||
[[ -n "$out" ]] && out="$out/"
|
||||
out="$out$(urlenc "$part")"
|
||||
done
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
resolve_drive() {
|
||||
local name="$1"
|
||||
if [[ "$name" == drv_* ]]; then
|
||||
echo "$name"
|
||||
return
|
||||
fi
|
||||
if [[ -n "$DRIVE_TOKEN" ]]; then
|
||||
die "drive tokens must reference drives by drv_ id; use account credentials to resolve drive names"
|
||||
fi
|
||||
if [[ "$name" == "default" || "$name" == "my-drive" || "$name" == "My Drive" ]]; then
|
||||
api_json GET "$BASE_URL/api/v1/drives/default" | "$JQ_BIN" -r '.drive.id'
|
||||
return
|
||||
fi
|
||||
local rows count
|
||||
rows=$(api_json GET "$BASE_URL/api/v1/drives" | "$JQ_BIN" --arg n "$name" '[.drives[] | select(.name == $n)]')
|
||||
count=$(echo "$rows" | "$JQ_BIN" 'length')
|
||||
[[ "$count" -eq 1 ]] || die "drive name '$name' matched $count drives; use a drv_ id"
|
||||
echo "$rows" | "$JQ_BIN" -r '.[0].id'
|
||||
}
|
||||
|
||||
drive_head() {
|
||||
local id="$1"
|
||||
api_json GET "$BASE_URL/api/v1/drives/$id" | "$JQ_BIN" -r '.drive.headVersionId // .headVersionId // empty'
|
||||
}
|
||||
|
||||
file_meta() {
|
||||
local id="$1"
|
||||
local path="$2"
|
||||
local prefix
|
||||
prefix=$(urlenc "$path")
|
||||
api_json GET "$BASE_URL/api/v1/drives/$id/files?prefix=$prefix&limit=200" | "$JQ_BIN" -c --arg p "$path" '.files[]? | select(.path == $p)' | head -n 1
|
||||
}
|
||||
|
||||
put_file() {
|
||||
local drive="$1"; shift
|
||||
local path="$1"; shift
|
||||
local local_file=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from) local_file="$2"; shift 2 ;;
|
||||
*) die "unexpected put argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ -f "$local_file" ]] || die "--from must be a file"
|
||||
local id sz ct sha meta body upload upload_url upload_id http_code
|
||||
id=$(resolve_drive "$drive")
|
||||
sz=$(wc -c < "$local_file" | tr -d ' ')
|
||||
[[ "$sz" -le "$MAX_FILE_BYTES" ]] || die "$path exceeds the $MAX_FILE_BYTES byte Drive file limit"
|
||||
ct=$(guess_content_type "$local_file")
|
||||
sha=$(compute_sha256 "$local_file")
|
||||
meta=$(file_meta "$id" "$path" || true)
|
||||
body=$("$JQ_BIN" -n --arg p "$path" --argjson s "$sz" --arg c "$ct" --arg sha "$sha" \
|
||||
'{path:$p,size:$s,contentType:$c,sha256:$sha}')
|
||||
if [[ -n "$meta" ]]; then
|
||||
etag=$(echo "$meta" | "$JQ_BIN" -r '.etag')
|
||||
body=$(echo "$body" | "$JQ_BIN" --arg e "$etag" '.ifMatch = $e')
|
||||
else
|
||||
body=$(echo "$body" | "$JQ_BIN" '.ifNoneMatch = "*"')
|
||||
fi
|
||||
upload=$(api_json POST "$BASE_URL/api/v1/drives/$id/files/uploads" "$body")
|
||||
upload_url=$(echo "$upload" | "$JQ_BIN" -r '.uploadUrl')
|
||||
upload_id=$(echo "$upload" | "$JQ_BIN" -r '.uploadId')
|
||||
http_code=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT "$upload_url" -H "Content-Type: $ct" --data-binary "@$local_file")
|
||||
[[ "$http_code" -ge 200 && "$http_code" -lt 300 ]] || die "upload failed for $path (HTTP $http_code)"
|
||||
api_json POST "$BASE_URL/api/v1/drives/$id/files/finalize" "$("$JQ_BIN" -n --arg u "$upload_id" '{uploadId:$u}')" | "$JQ_BIN" .
|
||||
}
|
||||
|
||||
case "$CMD" in
|
||||
create)
|
||||
name=""
|
||||
is_default="false"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--default) is_default="true"; shift ;;
|
||||
*) [[ -z "$name" ]] && name="$1" || die "unexpected argument: $1"; shift ;;
|
||||
esac
|
||||
done
|
||||
body=$("$JQ_BIN" -n --arg n "$name" --argjson d "$is_default" '{isDefault:$d} + (if $n == "" then {} else {name:$n} end)')
|
||||
api_json POST "$BASE_URL/api/v1/drives" "$body" | "$JQ_BIN" .
|
||||
;;
|
||||
default)
|
||||
api_json GET "$BASE_URL/api/v1/drives/default" | "$JQ_BIN" .
|
||||
;;
|
||||
ls)
|
||||
if [[ $# -eq 0 ]]; then
|
||||
[[ -z "$DRIVE_TOKEN" ]] || die "drive tokens cannot list drives; pass a drv_ id"
|
||||
api_json GET "$BASE_URL/api/v1/drives" | "$JQ_BIN" .
|
||||
else
|
||||
id=$(resolve_drive "$1")
|
||||
prefix="${2:-}"
|
||||
api_json GET "$BASE_URL/api/v1/drives/$id/files?prefix=$(urlenc "$prefix")" | "$JQ_BIN" .
|
||||
fi
|
||||
;;
|
||||
cat)
|
||||
[[ $# -eq 2 ]] || die "usage: drive.sh cat <drive> <path>"
|
||||
id=$(resolve_drive "$1")
|
||||
curl -fsS "$BASE_URL/api/v1/drives/$id/files/$(urlenc_path "$2")" "${auth_header[@]}"
|
||||
;;
|
||||
put)
|
||||
[[ $# -ge 2 ]] || die "usage: drive.sh put <drive> <path> --from <local-file>"
|
||||
put_file "$@"
|
||||
;;
|
||||
import)
|
||||
[[ $# -ge 2 ]] || die "usage: drive.sh import <drive> <prefix> --from <local-folder> [--dry-run]"
|
||||
drive="$1"; prefix="${2%/}"; shift 2
|
||||
from=""; dry=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--from) from="$2"; shift 2 ;;
|
||||
--dry-run) dry=1; shift ;;
|
||||
*) die "unexpected import argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ -d "$from" ]] || die "--from must be a folder"
|
||||
uploaded=0
|
||||
skipped=0
|
||||
failed=0
|
||||
planned=0
|
||||
while IFS= read -r -d '' f; do
|
||||
rel="${f#$from/}"
|
||||
[[ "$rel" == .git/* || "$rel" == node_modules/* || "$rel" == ".DS_Store" || "$rel" == */.DS_Store ]] && continue
|
||||
planned=$((planned + 1))
|
||||
sz=$(wc -c < "$f" | tr -d ' ')
|
||||
if [[ "$sz" -gt "$MAX_FILE_BYTES" ]]; then
|
||||
echo "skip oversized $f ($sz bytes > $MAX_FILE_BYTES)" >&2
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
dest="$rel"
|
||||
[[ -n "$prefix" ]] && dest="$prefix/$rel"
|
||||
if [[ "$dry" -eq 1 ]]; then
|
||||
echo "upload $f -> $dest"
|
||||
skipped=$((skipped + 1))
|
||||
else
|
||||
if (put_file "$drive" "$dest" --from "$f" >/dev/null); then
|
||||
uploaded=$((uploaded + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
fi
|
||||
done < <(find "$from" -type f -print0 | sort -z)
|
||||
echo "planned=$planned uploaded=$uploaded skipped=$skipped failed=$failed"
|
||||
[[ "$failed" -eq 0 ]] || exit 1
|
||||
;;
|
||||
export)
|
||||
[[ $# -ge 2 ]] || die "usage: drive.sh export <drive> <prefix> --to <local-folder> [--dry-run]"
|
||||
id=$(resolve_drive "$1"); prefix="${2%/}"; shift 2
|
||||
to=""; dry=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to) to="$2"; shift 2 ;;
|
||||
--dry-run) dry=1; shift ;;
|
||||
*) die "unexpected export argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$to" ]] || die "--to is required"
|
||||
cursor=""
|
||||
total=0
|
||||
while true; do
|
||||
url="$BASE_URL/api/v1/drives/$id/files?prefix=$(urlenc "$prefix")&limit=200"
|
||||
[[ -n "$cursor" ]] && url="$url&cursor=$(urlenc "$cursor")"
|
||||
files=$(api_json GET "$url")
|
||||
while IFS= read -r p; do
|
||||
[[ -n "$p" ]] || continue
|
||||
rel="$p"
|
||||
[[ -n "$prefix" ]] && rel="${p#$prefix/}"
|
||||
out="$to/$rel"
|
||||
if [[ "$dry" -eq 1 ]]; then
|
||||
echo "download $p -> $out"
|
||||
else
|
||||
mkdir -p "$(dirname "$out")"
|
||||
curl -fsS "$BASE_URL/api/v1/drives/$id/files/$(urlenc_path "$p")" "${auth_header[@]}" -o "$out"
|
||||
fi
|
||||
total=$((total + 1))
|
||||
done < <(echo "$files" | "$JQ_BIN" -r '.files[].path')
|
||||
cursor=$(echo "$files" | "$JQ_BIN" -r '.nextCursor // empty')
|
||||
[[ -n "$cursor" ]] || break
|
||||
done
|
||||
echo "files=$total"
|
||||
;;
|
||||
rm)
|
||||
[[ $# -ge 2 ]] || die "usage: drive.sh rm <drive> <path> [--recursive --confirm <path>]"
|
||||
id=$(resolve_drive "$1"); path="$2"; shift 2
|
||||
recursive=0; confirm=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--recursive) recursive=1; shift ;;
|
||||
--confirm) confirm="$2"; shift 2 ;;
|
||||
*) die "unexpected rm argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
if [[ "$recursive" -eq 1 ]]; then
|
||||
[[ "$confirm" == "$path" ]] || die "recursive delete requires --confirm '$path'"
|
||||
head=$(drive_head "$id")
|
||||
api_json DELETE "$BASE_URL/api/v1/drives/$id/files/$(urlenc_path "$path")?recursive=true&baseVersionId=$(urlenc "$head")" | "$JQ_BIN" .
|
||||
else
|
||||
meta=$(file_meta "$id" "$path")
|
||||
etag=$(echo "$meta" | "$JQ_BIN" -r '.etag')
|
||||
curl -fsS -X DELETE "$BASE_URL/api/v1/drives/$id/files/$(urlenc_path "$path")" "${auth_header[@]}" -H "If-Match: $etag" | "$JQ_BIN" .
|
||||
fi
|
||||
;;
|
||||
share)
|
||||
[[ $# -ge 1 ]] || die "usage: drive.sh share <drive> --perms read|write [--prefix notes/] [--ttl 30d] [--label text] [--manage-tokens]"
|
||||
id=$(resolve_drive "$1"); shift
|
||||
perms="write"; prefix=""; ttl=""; label=""; manage_tokens="false"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--perms) perms="$2"; shift 2 ;;
|
||||
--prefix) prefix="$2"; shift 2 ;;
|
||||
--ttl) ttl="$2"; shift 2 ;;
|
||||
--label) label="$2"; shift 2 ;;
|
||||
--manage-tokens) manage_tokens="true"; shift ;;
|
||||
*) die "unexpected share argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
body=$("$JQ_BIN" -n --arg p "$perms" --arg pp "$prefix" --arg ttl "$ttl" --arg label "$label" --argjson mt "$manage_tokens" \
|
||||
'{perms:$p} + (if $mt then {manageTokens:true} else {} end) + (if $ttl == "" then {} else {ttl:$ttl} end) + (if $pp == "" then {} else {pathPrefix:$pp} end) + (if $label == "" then {} else {label:$label} end)')
|
||||
api_json POST "$BASE_URL/api/v1/drives/$id/tokens" "$body" | "$JQ_BIN" -r '.shareBlock'
|
||||
;;
|
||||
tokens)
|
||||
[[ $# -eq 1 ]] || die "usage: drive.sh tokens <drive>"
|
||||
id=$(resolve_drive "$1")
|
||||
api_json GET "$BASE_URL/api/v1/drives/$id/tokens" | "$JQ_BIN" .
|
||||
;;
|
||||
revoke)
|
||||
[[ $# -eq 2 ]] || die "usage: drive.sh revoke <drive> <tokenId>"
|
||||
id=$(resolve_drive "$1")
|
||||
api_json DELETE "$BASE_URL/api/v1/drives/$id/tokens/$2" | "$JQ_BIN" .
|
||||
;;
|
||||
delete)
|
||||
[[ $# -ge 1 ]] || die "usage: drive.sh delete <drive> --confirm <drive name>"
|
||||
id=$(resolve_drive "$1"); shift
|
||||
confirm=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--confirm) confirm="$2"; shift 2 ;;
|
||||
*) die "unexpected delete argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
drive=$(api_json GET "$BASE_URL/api/v1/drives/$id")
|
||||
name=$(echo "$drive" | "$JQ_BIN" -r '.drive.name')
|
||||
[[ "$confirm" == "$name" ]] || die "delete requires --confirm '$name'"
|
||||
api_json DELETE "$BASE_URL/api/v1/drives/$id" | "$JQ_BIN" .
|
||||
;;
|
||||
*)
|
||||
die "unknown command: $CMD"
|
||||
;;
|
||||
esac
|
||||
|
|
@ -18,6 +18,8 @@ CLIENT=""
|
|||
TARGET=""
|
||||
FORKABLE=""
|
||||
SPA_MODE=""
|
||||
FROM_DRIVE=""
|
||||
DRIVE_VERSION=""
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
|
|
@ -33,6 +35,8 @@ Options:
|
|||
--client <name> Agent name for attribution (e.g. cursor, claude-code)
|
||||
--forkable Allow others to fork this site
|
||||
--spa Enable SPA routing
|
||||
--from-drive <drv_...> Publish a Drive snapshot instead of local files
|
||||
--version <dv_...> Drive version for --from-drive (default: current head)
|
||||
--base-url <url> API base (default: https://here.now)
|
||||
--allow-nonherenow-base-url
|
||||
Allow auth requests to non-default API base URL
|
||||
|
|
@ -71,14 +75,20 @@ while [[ $# -gt 0 ]]; do
|
|||
--allow-nonherenow-base-url) ALLOW_NON_HERENOW_BASE_URL=1; shift ;;
|
||||
--forkable) FORKABLE="true"; shift ;;
|
||||
--spa) SPA_MODE="true"; shift ;;
|
||||
--from-drive) FROM_DRIVE="$2"; shift 2 ;;
|
||||
--version) DRIVE_VERSION="$2"; shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
-*) die "unknown option: $1" ;;
|
||||
*) [[ -z "$TARGET" ]] && TARGET="$1" || die "unexpected argument: $1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$TARGET" ]] || usage
|
||||
[[ -e "$TARGET" ]] || die "path does not exist: $TARGET"
|
||||
if [[ -n "$FROM_DRIVE" ]]; then
|
||||
[[ -z "$TARGET" ]] || die "--from-drive does not accept a local file-or-dir argument"
|
||||
else
|
||||
[[ -n "$TARGET" ]] || usage
|
||||
[[ -e "$TARGET" ]] || die "path does not exist: $TARGET"
|
||||
fi
|
||||
|
||||
# Load API key from credentials file if not provided via flag or env
|
||||
if [[ -z "$API_KEY" && -f "$CREDENTIALS_FILE" ]]; then
|
||||
|
|
@ -100,6 +110,57 @@ if [[ -n "$SLUG" && -z "$CLAIM_TOKEN" && -z "$API_KEY" && -f "$STATE_FILE" ]]; t
|
|||
CLAIM_TOKEN=$("$JQ_BIN" -r --arg s "$SLUG" '.publishes[$s].claimToken // empty' "$STATE_FILE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -n "$FROM_DRIVE" ]]; then
|
||||
[[ -n "$API_KEY" ]] || die "--from-drive requires an account API key"
|
||||
BODY=$("$JQ_BIN" -n --arg d "$FROM_DRIVE" '{driveId:$d}')
|
||||
[[ -n "$DRIVE_VERSION" ]] && BODY=$(echo "$BODY" | "$JQ_BIN" --arg v "$DRIVE_VERSION" '.versionId = $v')
|
||||
[[ -n "$SLUG" ]] && BODY=$(echo "$BODY" | "$JQ_BIN" --arg s "$SLUG" '.slug = $s')
|
||||
if [[ -n "$TITLE" || -n "$DESCRIPTION" ]]; then
|
||||
viewer="{}"
|
||||
[[ -n "$TITLE" ]] && viewer=$(echo "$viewer" | "$JQ_BIN" --arg t "$TITLE" '.title = $t')
|
||||
[[ -n "$DESCRIPTION" ]] && viewer=$(echo "$viewer" | "$JQ_BIN" --arg d "$DESCRIPTION" '.description = $d')
|
||||
BODY=$(echo "$BODY" | "$JQ_BIN" --argjson v "$viewer" '.viewer = $v')
|
||||
fi
|
||||
[[ "$FORKABLE" == "true" ]] && BODY=$(echo "$BODY" | "$JQ_BIN" '.forkable = true')
|
||||
[[ "$SPA_MODE" == "true" ]] && BODY=$(echo "$BODY" | "$JQ_BIN" '.spaMode = true')
|
||||
CLIENT_HEADER_VALUE="here-now-publish-sh"
|
||||
if [[ -n "$CLIENT" ]]; then
|
||||
normalized_client=$(echo "$CLIENT" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-')
|
||||
normalized_client="${normalized_client#-}"
|
||||
normalized_client="${normalized_client%-}"
|
||||
if [[ -n "$normalized_client" ]]; then
|
||||
CLIENT_HEADER_VALUE="${normalized_client}/publish-sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "publishing from Drive..." >&2
|
||||
RESPONSE=$(curl -sS -X POST "$BASE_URL/api/v1/publish/from-drive" \
|
||||
-H "authorization: Bearer $API_KEY" \
|
||||
-H "x-herenow-client: $CLIENT_HEADER_VALUE" \
|
||||
-H "content-type: application/json" \
|
||||
-d "$BODY")
|
||||
if echo "$RESPONSE" | "$JQ_BIN" -e '.error' >/dev/null 2>&1; then
|
||||
err=$(echo "$RESPONSE" | "$JQ_BIN" -r '.error')
|
||||
die "$err"
|
||||
fi
|
||||
SITE_URL=$(echo "$RESPONSE" | "$JQ_BIN" -r '.siteUrl')
|
||||
OUT_SLUG=$(echo "$RESPONSE" | "$JQ_BIN" -r '.slug')
|
||||
CURRENT_VERSION=$(echo "$RESPONSE" | "$JQ_BIN" -r '.currentVersionId')
|
||||
DRIVE_VERSION_OUT=$(echo "$RESPONSE" | "$JQ_BIN" -r '.driveVersionId')
|
||||
echo "$SITE_URL"
|
||||
echo "" >&2
|
||||
echo "publish_result.site_url=$SITE_URL" >&2
|
||||
echo "publish_result.slug=$OUT_SLUG" >&2
|
||||
echo "publish_result.action=from_drive" >&2
|
||||
echo "publish_result.auth_mode=authenticated" >&2
|
||||
echo "publish_result.api_key_source=$API_KEY_SOURCE" >&2
|
||||
echo "publish_result.persistence=permanent" >&2
|
||||
echo "publish_result.drive_id=$FROM_DRIVE" >&2
|
||||
echo "publish_result.drive_version_id=$DRIVE_VERSION_OUT" >&2
|
||||
echo "publish_result.current_version_id=$CURRENT_VERSION" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
compute_sha256() {
|
||||
local f="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue