Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<p><strong>Agent-focused CLI for the <a href="https://polylane.com">Polylane</a> platform.</strong><br>
Investigate production issues, explore cloud infrastructure, search code, run automations, and drive threads — from any agent or terminal.</p>

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

<p>📚 <strong>Docs: <a href="https://docs.polylane.com">docs.polylane.com</a></strong> · <a href="https://docs.polylane.com/getting-started">Getting started</a> · <a href="https://docs.polylane.com/llms.txt">llms.txt</a> (agent index)</p>

<p>
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
110 changes: 14 additions & 96 deletions install/install.sh
Original file line number Diff line number Diff line change
@@ -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 -- "$@"
1 change: 1 addition & 0 deletions src/commands/telemetry/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down
1 change: 1 addition & 0 deletions src/config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions src/telemetry/environment.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/telemetry/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -27,6 +27,7 @@ export interface CliTelemetryEvent {
version: string;
clientKind: 'cli';
installSource: InstallSource;
ref?: string; // install referral slug from ~/.polylane/ref, if valid
};

runtime: {
Expand Down Expand Up @@ -127,6 +128,7 @@ export function buildEvent(input: BuildEventInput): CliTelemetryEvent {
version: getCliVersion(),
clientKind: 'cli',
installSource: detectInstallSource(),
...(readInstallRef() ? { ref: readInstallRef()! } : {}),
},
runtime: {
node: process.versions.node,
Expand Down
Loading