OverviewFilesCommitsIssuesDocsReleasesPeers

pgh commits / 1af93dd

0.1.139: make the documented fresh-machine quick start actually work
Two independent reasons `curl install.sh | sh` -> `pgh app install pear-git-v3` could not work on a machine that had never run pgh. 1. Bare app names could not resolve. Name resolution is subscription- based (sidecar `_resolveNameAcross` = own registry, then each subscribed replica), but NOTHING seeded a subscription — not init, not install.sh, not any package.json default. A fresh peer booted its own empty NameRegistry with zero replicas, so every documented `pgh app install <name>` failed with "name not found in pgh-sidecar's NameRegistry"; only an explicit pgh://<registry-key>/<name> worked. Adds DEFAULT_NAME_REGISTRIES to lib/bootstrap.js (the existing home for initial-install constants). The sidecar falls back to it only when both subscriptions.json and PGH_NAME_REGISTRIES are empty — configuration WINS OUTRIGHT rather than merging, so a peer running its own namespace registries never has ours forced on it. Read-only: subscribing does not grant claim rights here, nor remove them from the peer's own registry. 2. PATH. The launchers land in <prefix>/bin, and if that is not on PATH the next command of the quick start (`pgh init`) is "command not found" — and init cannot be the fix, since you cannot invoke what is not found. Debian/Ubuntu's stock ~/.profile adds ~/.local/bin only `if [ -d ]` at LOGIN, and install.sh creates that dir, so a first-ever install has the directory but not the PATH entry until next login. install.sh now appends an idempotent guarded block to the login shell's rc (bash/zsh/fish/profile), with --no-modify-path to opt out; --no-path-warn keeps its "leave my PATH alone" meaning and implies it. Skipped in managed/recover mode. Since an rc edit cannot affect the running shell, next-steps print absolute paths whenever PATH is missing, so the printed sequence works verbatim either way. Also makes subscriptions.json path overridable via PGH_SUBSCRIPTIONS_PATH — needed for a hermetic test, and it closes a gap in the PGH_SIDECAR_* isolation story where an experimental sidecar shared prod's subscriptions file. Verified: install.sh rc-append, guard-line correctness when sourced, double-source non-duplication, rerun idempotency, already-on-PATH no-op, --no-modify-path, --no-path-warn, zsh/fish targeting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1af93dd01a8c7e2faf3b47927cdff4847e4e6e52
by plankatron <fleeky@gmail.com> · 2026-08-09T00:42:41.000Z

Changed files (4) +116 -23

lib/bootstrap.js +21 -0
……@@ -27,6 +27,27 @@ import crypto from 'hypercore-crypto'
2727export const BOOTSTRAP_VERSION = 'pgh-bootstrap-v1'
2828export const BOOTSTRAP_TOPIC = crypto.hash(Buffer.from(BOOTSTRAP_VERSION))
2929
30/**
31 * NameRegistry autobases a fresh sidecar replicates read-only by default.
32 *
33 * Name resolution is SUBSCRIPTION-based, not global-singleton: `resolve-name`
34 * searches the sidecar's own registry first, then each subscribed replica
35 * (pgh-sidecar `_resolveNameAcross`). Anyone can run their own namespace
36 * registry and publish its key; this list is only the default one shipped
37 * with pgh, so `pgh app install <name>` resolves out of the box instead of
38 * failing against a fresh peer's own empty registry.
39 *
40 * Read-only — subscribing does NOT let this peer claim names here, and it
41 * does not stop them claiming names in their own registry.
42 *
43 * Overridden by an explicit `nameRegistries` in ~/.config/pgh/subscriptions.json
44 * or PGH_NAME_REGISTRIES; those win outright rather than merging, so a peer
45 * that wants a different namespace set never has this one forced on it.
46 */
47export const DEFAULT_NAME_REGISTRIES = [
48 '3c702c3a6767fa507523caed7b992bc9b0bc38638bd4ca0d3a7f79152e68e9d1'
49]
50
3051/**
3152 * Resolve a user-supplied bootstrap spec into { kind, value }.
3253 *
lib/subscriptions.js +16 -7
……@@ -21,15 +21,24 @@
2121 * resolves. `pgh names subscribe <key>` adds one here.
2222 */
2323
24import process from '#node:process'
2425import fs from '#node:fs'
2526import path from '#node:path'
2627import { homedir } from '#node:os'
2728
2829export const SUBSCRIPTIONS_PATH = path.join(homedir(), '.config', 'pgh', 'subscriptions.json')
2930
31// Resolved per call (not once at import) so tests and an isolated sidecar can
32// point at their own file. Mirrors the PGH_SIDECAR_* overrides in
33// pgh-sidecar/bin — subscriptions.json was the one piece of that isolation
34// story with no override, so an experimental sidecar shared prod's file.
35function subscriptionsPath () {
36 return process.env.PGH_SUBSCRIPTIONS_PATH || SUBSCRIPTIONS_PATH
37}
38
3039export function readSubscriptions () {
3140 try {
32 const raw = fs.readFileSync(SUBSCRIPTIONS_PATH, 'utf8')
41 const raw = fs.readFileSync(subscriptionsPath(), 'utf8')
3342 const j = JSON.parse(raw)
3443 return Array.isArray(j.registries) ? j.registries.filter(k => typeof k === 'string' && k.length === 64) : []
3544 } catch { return [] }
……@@ -37,10 +46,10 @@ export function readSubscriptions () {
3746
3847function writeSubscriptions (keys) {
3948 // Preserve sibling keys (e.g. nameRegistries) — don't clobber the file.
40 const j = (() => { try { return JSON.parse(fs.readFileSync(SUBSCRIPTIONS_PATH, 'utf8')) || {} } catch { return {} } })()
49 const j = (() => { try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} } catch { return {} } })()
4150 j.registries = [...new Set(keys)]
42 fs.mkdirSync(path.dirname(SUBSCRIPTIONS_PATH), { recursive: true })
43 fs.writeFileSync(SUBSCRIPTIONS_PATH, JSON.stringify(j, null, 2))
51 fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
52 fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
4453}
4554
4655export function addSubscription (keyHex) {
……@@ -66,15 +75,15 @@ export function removeSubscription (keyHex) {
6675// works across publishers).
6776
6877function readRaw () {
69 try { return JSON.parse(fs.readFileSync(SUBSCRIPTIONS_PATH, 'utf8')) || {} }
78 try { return JSON.parse(fs.readFileSync(subscriptionsPath(), 'utf8')) || {} }
7079 catch { return {} }
7180}
7281
7382function writeNameSubscriptions (keys) {
7483 const j = readRaw()
7584 j.nameRegistries = [...new Set(keys)]
76 fs.mkdirSync(path.dirname(SUBSCRIPTIONS_PATH), { recursive: true })
77 fs.writeFileSync(SUBSCRIPTIONS_PATH, JSON.stringify(j, null, 2))
85 fs.mkdirSync(path.dirname(subscriptionsPath()), { recursive: true })
86 fs.writeFileSync(subscriptionsPath(), JSON.stringify(j, null, 2))
7887}
7988
8089export function readNameSubscriptions () {
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "pgh",
3 "version": "0.1.138",
3 "version": "0.1.139",
44 "description": "pgh \u2014 P2P content-addressed package manager and app launcher",
55 "type": "module",
66 "main": "index.js",
scripts/install.sh +78 -15
……@@ -41,7 +41,8 @@
4141# --bin-name <name> rename pgh's bin (sidecar follows as <name>-sidecar)
4242# --base <url> download base (default: $PGH_DOWNLOAD_BASE or
4343# https://7rnx.net/git/bootstrap)
44# --no-path-warn skip the PATH-not-set warning
44# --no-modify-path don't add <prefix>/bin to your shell rc
45# --no-path-warn skip the PATH-not-set warning (implies --no-modify-path)
4546#
4647# Nothing here requires root.
4748
……@@ -57,7 +58,13 @@ FROM_DIR="" # --from-dir: local dir of tarballs
5758ONLY_APP="" # --app: install just one
5859BASE="$DEFAULT_BASE"
5960SKIP_PATH_WARN=0
61MODIFY_PATH=1 # append BIN_DIR to the login shell's rc when it's missing
6062FORCE_MODE="" # --recover | --fresh | "" (auto-detect)
63
64# Idempotency markers for the block we manage in the user's shell rc. Never
65# change these — a rerun finds its own previous block by exact match.
66PATH_MARKER_BEGIN='# >>> pgh PATH >>>'
67PATH_MARKER_END='# <<< pgh PATH <<<'
6168BIN_NAME_SET=0 # did the caller pass --bin-name explicitly?
6269
6370# Managed-peer recovery targets the pgh-managed tree store, not the fresh-
……@@ -76,8 +83,11 @@ while [ $# -gt 0 ]; do
7683 --base) BASE="$2"; shift 2 ;;
7784 --recover) FORCE_MODE="recover"; shift ;;
7885 --fresh) FORCE_MODE="fresh"; shift ;;
79 --no-path-warn) SKIP_PATH_WARN=1; shift ;;
80 -h|--help) sed -n '2,46p' "$0"; exit 0 ;;
86 --no-modify-path) MODIFY_PATH=0; shift ;;
87 # Historically the only "leave my PATH alone" flag, so it keeps that
88 # meaning in full: suppress the warning AND touch no rc file.
89 --no-path-warn) SKIP_PATH_WARN=1; MODIFY_PATH=0; shift ;;
90 -h|--help) sed -n '2,47p' "$0"; exit 0 ;;
8191 *) echo "unknown flag: $1" >&2; exit 1 ;;
8292 esac
8393done
……@@ -272,17 +282,55 @@ else
272282 install_one pgh-sidecar "${BIN_NAME}-sidecar"
273283fi
274284
275# ── PATH check ────────────────────────────────────────────────
276if [ "$SKIP_PATH_WARN" -ne 1 ]; then
277 case ":$PATH:" in
278 *":${BIN_DIR}:"*) : ;;
279 *)
280 echo
281 echo "WARNING: ${BIN_DIR} is not on your PATH. Add to your shell rc:"
282 echo " export PATH=\"${BIN_DIR}:\$PATH\""
283 echo
284 ;;
285# ── PATH ──────────────────────────────────────────────────────
286# The launchers we just linked live in BIN_DIR. If it isn't on PATH then the
287# next command of every documented quick start — `pgh init` — is "command not
288# found", and `pgh init` CANNOT be the place that fixes it: you can't invoke
289# what isn't found. So the installer owns PATH (rustup's model), unless
290# --no-modify-path / --no-path-warn.
291#
292# Why this bites on a machine that looks fine: Debian/Ubuntu's stock ~/.profile
293# adds ~/.local/bin only `if [ -d ... ]`, evaluated at LOGIN. We create that
294# directory here (mkdir -p above), so a first-ever install on such a box has
295# the dir but not the PATH entry until the next login.
296#
297# An rc edit never affects the ALREADY-RUNNING shell, so when PATH is missing
298# the next-steps below print absolute paths — the sequence works verbatim
299# either way.
300PATH_OK=0
301case ":$PATH:" in *":${BIN_DIR}:"*) PATH_OK=1 ;; esac
302
303PATH_RC="" # rc file we appended to (empty = none)
304if [ "$PATH_OK" -eq 0 ] && [ "$MODIFY_PATH" -eq 1 ] && [ "$MANAGED" -ne 1 ]; then
305 # Target the rc of the user's LOGIN shell, not whatever is running us (curl
306 # | sh means $0's shell is always sh).
307 case "$(basename "${SHELL:-/bin/sh}")" in
308 zsh) rc="${ZDOTDIR:-$HOME}/.zshrc" ;;
309 bash) rc="${HOME}/.bashrc" ;;
310 fish) rc="${HOME}/.config/fish/config.fish" ;;
311 *) rc="${HOME}/.profile" ;;
285312 esac
313
314 if [ -f "$rc" ] && grep -qF "$PATH_MARKER_BEGIN" "$rc" 2>/dev/null; then
315 PATH_RC="$rc" # already managed by a previous run — idempotent, no-op
316 echo " PATH block already present in ${rc}"
317 else
318 mkdir -p "$(dirname "$rc")"
319 # The appended line is itself guarded, so re-sourcing the rc (or a login
320 # shell that already has BIN_DIR from elsewhere) never duplicates the entry.
321 {
322 echo ""
323 echo "$PATH_MARKER_BEGIN"
324 if [ "$(basename "${SHELL:-/bin/sh}")" = "fish" ]; then
325 echo "fish_add_path \"${BIN_DIR}\""
326 else
327 echo "case \":\$PATH:\" in *\":${BIN_DIR}:\"*) ;; *) export PATH=\"${BIN_DIR}:\$PATH\" ;; esac"
328 fi
329 echo "$PATH_MARKER_END"
330 } >> "$rc"
331 PATH_RC="$rc"
332 echo " added ${BIN_DIR} to PATH in ${rc}"
333 fi
286334fi
287335
288336echo
……@@ -293,12 +341,27 @@ if [ "$MANAGED" -eq 1 ]; then
293341 echo
294342 echo "Verify: ${BIN_NAME} app status"
295343else
344 # Absolute paths whenever PATH is missing in THIS shell — including right
345 # after we edited an rc file, which only takes effect in new shells.
346 if [ "$PATH_OK" -eq 1 ]; then PGH_CMD="$BIN_NAME"; else PGH_CMD="${BIN_DIR}/${BIN_NAME}"; fi
347
296348 echo "pgh installed. Next steps:"
297349 echo
298 echo " ${BIN_NAME} init # identity + tree store + local registries"
299 echo " ${BIN_NAME} app boot # start pgh-sidecar (and any autostart apps)"
350 echo " ${PGH_CMD} init # identity + tree store + local registries"
351 echo " ${PGH_CMD} app boot # start pgh-sidecar (and any autostart apps)"
300352 echo
301353 echo "init sets things up but does NOT start the sidecar — 'app boot' does."
354
355 if [ "$PATH_OK" -eq 0 ] && [ "$SKIP_PATH_WARN" -ne 1 ]; then
356 echo
357 if [ -n "$PATH_RC" ]; then
358 echo "PATH: ${BIN_DIR} added to ${PATH_RC} — applies to NEW shells."
359 echo " For this one: export PATH=\"${BIN_DIR}:\$PATH\""
360 else
361 echo "WARNING: ${BIN_DIR} is not on your PATH. Add to your shell rc:"
362 echo " export PATH=\"${BIN_DIR}:\$PATH\""
363 fi
364 fi
302365fi
303366echo
304367