From 91c0a4f760b899cf698ad93cb71e476d03ce45a3 Mon Sep 17 00:00:00 2001 From: Boris Tane Date: Sun, 26 Jul 2026 14:47:02 -0700 Subject: [PATCH] feat: install referral telemetry, README positioning, installer shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The website installer records a first-touch referral slug to ~/.polylane/ref (from polylane.com/install?ref=โ€ฆ). Telemetry reads it, validates it against ^[a-zA-Z0-9._-]{1,64}$, and sends it as the optional cli.ref field beside installSource, so installs are attributable to the surface that produced them (directory listing, landing page, README). Missing or invalid refs are silently omitted; the read is cached and can never break a command. PRIVACY.md and `polylane telemetry status` document the new field. README: adds the channel positioning line ("Your coding agent writes the code. It can't see production. Polylane is the missing sense."), the `npx skills add coreplanelabs/cli` path, and fixes "anomalies" to "detections" per the product-wide rename. install/install.sh becomes a delegating shim: the authoritative installer lives on the website at polylane.com/install; this file only keeps old links and raw fetches working (env vars and flags pass through, fetch failures are fatal rather than piping nothing into bash). Co-Authored-By: Claude Fable 5 --- PRIVACY.md | 1 + README.md | 6 +- install/install.sh | 110 ++++--------------------------- src/commands/telemetry/status.ts | 1 + src/config/paths.ts | 1 + src/telemetry/environment.ts | 22 +++++++ src/telemetry/event.ts | 4 +- 7 files changed, 47 insertions(+), 98 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index d8b7fe8..08f7291 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -27,6 +27,7 @@ One event per command invocation, fired after the command finishes (success or f |---|---|---| | `cli.version` | `"0.1.0"` | CLI release in use | | `cli.installSource` | `"brew"` | npm / brew / curl / bun / dev / unknown | +| `cli.ref` | `"gh-readme"` | Optional referral slug the website installer recorded in `~/.polylane/ref` (validated against `^[a-zA-Z0-9._-]{1,64}$`, omitted when absent) | ### Runtime diff --git a/README.md b/README.md index da1af8e..ea2ecfe 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@

Agent-focused CLI for the Polylane platform.
Investigate production issues, explore cloud infrastructure, search code, run automations, and drive threads โ€” from any agent or terminal.

+

Your coding agent writes the code. It can't see production. Polylane is the missing sense.

+

๐Ÿ“š Docs: docs.polylane.com ยท Getting started ยท llms.txt (agent index)

@@ -15,7 +17,7 @@ Investigate production issues, explore cloud infrastructure, search code, run au `polylane` is designed to be driven by AI agents. Top-level commands map to the tasks an agent actually performs: -- **Triage** issues โ€” anomalies and alerts detected across your cloud and observability stack +- **Triage** issues โ€” detections and alerts across your cloud and observability stack - **Drive** issue timelines โ€” record notes and milestones as you respond - **Explore** cloud infrastructure โ€” logs, metrics, dependency graphs - **Search** code @@ -79,6 +81,8 @@ polylane setup --project # this project instead of the home dir It is idempotent: the skill is overwritten with the version bundled in the CLI, an existing MCP entry is left untouched, and unreadable config files are skipped with a manual pointer instead of clobbered. The curl installer runs it automatically after install (opt out with `--no-setup`). +The skill alone, without the CLI: `npx skills add coreplanelabs/cli`. + ## Quick start ```bash diff --git a/install/install.sh b/install/install.sh index 5d155c3..1009303 100644 --- a/install/install.sh +++ b/install/install.sh @@ -1,103 +1,21 @@ -#!/usr/bin/env bash -# polylane CLI installer for macOS / Linux. +#!/bin/sh +# polylane CLI installer shim for macOS / Linux. # -# curl -fsSL https://polylane.com/install.sh | bash +# The authoritative installer is maintained on the website and served at +# https://polylane.com/install โ€” this file only delegates to it so old links +# and raw fetches of this path keep working. # -# Installs the bundled CLI to ~/.polylane/bin/polylane and (if needed) prints the -# line to add to your shell RC to put it on $PATH. Node 20+ must be installed. -# Override the version or install prefix with env vars: +# Env vars (POLYLANE_VERSION, POLYLANE_PREFIX, POLYLANE_REF, +# POLYLANE_SKIP_SETUP) and flags (--no-setup) pass straight through: # -# POLYLANE_VERSION=v0.1.0 curl -fsSL https://polylane.com/install.sh | bash -# POLYLANE_PREFIX=/usr/local/bin curl -fsSL https://polylane.com/install.sh | sudo bash +# POLYLANE_VERSION=v0.2.1 curl -fsSL https://polylane.com/install | bash -set -euo pipefail +set -eu -# --- config ----------------------------------------------------------------- +command -v curl >/dev/null 2>&1 || { printf 'error: curl is required\n' >&2; exit 1; } +command -v bash >/dev/null 2>&1 || { printf 'error: bash is required\n' >&2; exit 1; } -REPO="coreplanelabs/cli" # GitHub owner/repo -BIN_NAME="polylane" -VERSION="${POLYLANE_VERSION:-latest}" -PREFIX_DIR="${POLYLANE_PREFIX:-$HOME/.polylane/bin}" -BUNDLE_ASSET="polylane.mjs" +script="$(curl -fsSL https://polylane.com/install)" \ + || { printf 'error: failed to fetch https://polylane.com/install\n' >&2; exit 1; } -# --- helpers ---------------------------------------------------------------- - -die() { printf '\033[31merror\033[0m: %s\n' "$*" >&2; exit 1; } -info() { printf '\033[2m%s\033[0m\n' "$*"; } -ok() { printf '\033[32mโœ“\033[0m %s\n' "$*"; } - -require_cmd() { - command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" -} - -# --- preflight -------------------------------------------------------------- - -require_cmd curl -require_cmd uname - -if ! command -v node >/dev/null 2>&1; then - die "Node.js 20+ is required but 'node' was not found. - Install from https://nodejs.org or with your package manager, then re-run." -fi - -NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))')" -if [ "$NODE_MAJOR" -lt 20 ]; then - die "Node.js 20+ is required (found $(node -v))." -fi - -# --- resolve download URL --------------------------------------------------- - -if [ "$VERSION" = "latest" ]; then - # Use the "latest" redirect GitHub serves for releases - DOWNLOAD_URL="https://github.com/$REPO/releases/latest/download/$BUNDLE_ASSET" -else - DOWNLOAD_URL="https://github.com/$REPO/releases/download/$VERSION/$BUNDLE_ASSET" -fi - -# --- download -------------------------------------------------------------- - -TMP_DIR="$(mktemp -d)" -trap 'rm -rf "$TMP_DIR"' EXIT -TMP_FILE="$TMP_DIR/$BUNDLE_ASSET" - -info "Downloading $DOWNLOAD_URL" -if ! curl -fsSL --retry 3 --connect-timeout 10 "$DOWNLOAD_URL" -o "$TMP_FILE"; then - die "download failed โ€” check your network and that the release exists" -fi - -if ! head -1 "$TMP_FILE" | grep -q '^#!'; then - die "downloaded file does not look like a polylane CLI bundle" -fi - -# --- install --------------------------------------------------------------- - -mkdir -p "$PREFIX_DIR" -TARGET="$PREFIX_DIR/$BIN_NAME" -install -m 0755 "$TMP_FILE" "$TARGET" -ok "installed $TARGET" - -INSTALLED_VERSION="$("$TARGET" --version 2>/dev/null || echo unknown)" -ok "polylane $INSTALLED_VERSION" - -# --- PATH hint ------------------------------------------------------------- - -case ":$PATH:" in - *":$PREFIX_DIR:"*) ;; - *) - RC_HINT="" - case "${SHELL:-}" in - *zsh) RC_HINT="$HOME/.zshrc" ;; - *bash) RC_HINT="$HOME/.bashrc" ;; - *fish) RC_HINT="$HOME/.config/fish/config.fish" ;; - esac - printf '\n' - info "Add %s to your PATH:" "$PREFIX_DIR" - printf ' export PATH="%s:$PATH"\n' "$PREFIX_DIR" - if [ -n "$RC_HINT" ]; then - info "For example:" - printf ' echo '\''export PATH="%s:$PATH"'\'' >> %s\n' "$PREFIX_DIR" "$RC_HINT" - fi - ;; -esac - -printf '\nRun \033[1mpolylane --help\033[0m to get started.\n' +printf '%s\n' "$script" | bash -s -- "$@" diff --git a/src/commands/telemetry/status.ts b/src/commands/telemetry/status.ts index 6537066..f28840c 100644 --- a/src/commands/telemetry/status.ts +++ b/src/commands/telemetry/status.ts @@ -32,6 +32,7 @@ export const telemetryStatusCommand: Command = { ], runtime: [ 'CLI version + install source (npm/brew/curl/bun/dev/unknown)', + 'install referral slug, only if the website installer wrote ~/.polylane/ref', 'Node version, OS + OS release, arch', 'CI flag + CI provider name (GitHub Actions / CircleCI / ...) when applicable', 'TTY flags (stdout, stderr)', diff --git a/src/config/paths.ts b/src/config/paths.ts index acd49e3..6fe84d4 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -7,6 +7,7 @@ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); export const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json'); export const UPDATE_STATE_FILE = join(CONFIG_DIR, 'update-state.json'); export const TELEMETRY_STATE_FILE = join(CONFIG_DIR, 'telemetry.json'); +export const INSTALL_REF_FILE = join(CONFIG_DIR, 'ref'); export function ensureConfigDir(): void { ensureDir(CONFIG_DIR, 0o700); diff --git a/src/telemetry/environment.ts b/src/telemetry/environment.ts index 543d043..41855b7 100644 --- a/src/telemetry/environment.ts +++ b/src/telemetry/environment.ts @@ -1,5 +1,7 @@ import { sep } from 'node:path'; import { release as osRelease, cpus as osCpus, totalmem } from 'node:os'; +import { readFileSync } from 'node:fs'; +import { INSTALL_REF_FILE } from '../config/paths'; export type InstallSource = 'npm' | 'bun' | 'brew' | 'curl' | 'dev' | 'unknown'; @@ -29,6 +31,26 @@ export function detectInstallSource(): InstallSource { return 'unknown'; } +// Referral slug written once by the website installer (~/.polylane/ref). +// Purely for distribution-channel attribution โ€” never generated by the CLI. +// Cached per process; any read failure or invalid content means "no ref". +const INSTALL_REF_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/; + +let cachedInstallRef: string | null | undefined; + +export function readInstallRef(): string | null { + if (cachedInstallRef === undefined) { + cachedInstallRef = null; + try { + const raw = readFileSync(INSTALL_REF_FILE, 'utf-8').trim(); + if (INSTALL_REF_PATTERN.test(raw)) cachedInstallRef = raw; + } catch { + // missing / unreadable โ€” telemetry must never break the CLI + } + } + return cachedInstallRef; +} + export interface EnvironmentInfo { shell: string | null; // "zsh", "bash", "fish", "pwsh", null terminal: string | null; // TERM_PROGRAM, e.g. "iTerm.app", "vscode", "Apple_Terminal", null diff --git a/src/telemetry/event.ts b/src/telemetry/event.ts index bbb326d..a2bca77 100644 --- a/src/telemetry/event.ts +++ b/src/telemetry/event.ts @@ -4,7 +4,7 @@ import type { Config } from '../config/schema'; import type { Credential } from '../auth/types'; import { isCI, getCIName, isStdoutTTY, isStderrTTY } from '../utils/env'; import { getInstallId, recordRun } from './state'; -import { detectInstallSource, detectEnvironment, type InstallSource } from './environment'; +import { detectInstallSource, detectEnvironment, readInstallRef, type InstallSource } from './environment'; import { getHttpStats } from './http-counter'; import { getCliVersion } from '../version'; @@ -27,6 +27,7 @@ export interface CliTelemetryEvent { version: string; clientKind: 'cli'; installSource: InstallSource; + ref?: string; // install referral slug from ~/.polylane/ref, if valid }; runtime: { @@ -127,6 +128,7 @@ export function buildEvent(input: BuildEventInput): CliTelemetryEvent { version: getCliVersion(), clientKind: 'cli', installSource: detectInstallSource(), + ...(readInstallRef() ? { ref: readInstallRef()! } : {}), }, runtime: { node: process.versions.node,