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,