Use Railway Railway resource model Railway organizes infrastructure in a hierarchy: - Workspace is the billing and team scope. A user belongs to one or more workspaces. - Project is a collection of services under one workspace. It maps to one deployable unit of work. - Environment is an isolated configuration plane inside a project (for example, , ). Each environment has its own variables, config, and deployment history. - Service is a single deployable unit inside a project. It can be an app from a repo, a Docker image, or a managed database. - Bucket is an S3-compatible object storage resou…

, key):\n continue\n # Parse keys=X,expires=Y,avg_ttl=Z\n parts = {}\n for item in value.split(\",\"):\n k, _, v = item.partition(\"=\")\n parts[k] = v\n keys = _safe_int(parts.get(\"keys\"))\n total_keys += keys\n databases.append({\n \"db\": key,\n \"keys\": keys,\n \"expires\": _safe_int(parts.get(\"expires\")),\n \"avg_ttl\": _safe_int(parts.get(\"avg_ttl\")),\n })\n return databases, total_keys\n\n\ndef extract_command_stats(info: Dict[str, str]) -> List[Dict[str, Any]]:\n \"\"\"Extract command statistics from INFO dict.\n\n Command stat entries look like: cmdstat_GET:calls=123,usec=456,usec_per_call=3.71\n Returns list sorted by calls descending.\n \"\"\"\n stats: List[Dict[str, Any]] = []\n for key, value in info.items():\n if not key.startswith(\"cmdstat_\"):\n continue\n cmd_name = key[len(\"cmdstat_\"):]\n parts = {}\n for item in value.split(\",\"):\n k, _, v = item.partition(\"=\")\n parts[k] = v\n stats.append({\n \"command\": cmd_name,\n \"calls\": _safe_int(parts.get(\"calls\")),\n \"usec\": _safe_int(parts.get(\"usec\")),\n \"usec_per_call\": _safe_float(parts.get(\"usec_per_call\")),\n })\n stats.sort(key=lambda x: x[\"calls\"], reverse=True)\n return stats\n\n\n_CLIENT_IP_RE = re.compile(r'^(\\[.+\\]|(?:\\d{1,3}\\.){3}\\d{1,3}):\\d+

Use Railway Railway resource model Railway organizes infrastructure in a hierarchy: - Workspace is the billing and team scope. A user belongs to one or more workspaces. - Project is a collection of services under one workspace. It maps to one deployable unit of work. - Environment is an isolated configuration plane inside a project (for example, , ). Each environment has its own variables, config, and deployment history. - Service is a single deployable unit inside a project. It can be an app from a repo, a Docker image, or a managed database. - Bucket is an S3-compatible object storage resou…

)\n\n\ndef parse_slowlog_get(raw: str) -> List[Dict[str, Any]]:\n \"\"\"Parse SLOWLOG GET output into structured entries.\n\n redis-cli --raw SLOWLOG GET format (Redis 4.0+):\n \u003cid>\n \u003ctimestamp_unix>\n \u003cduration_us>\n \u003ccmd>\n \u003carg1>\n ...\n \u003cargN>\n \u003cclient_ip:port>\n [\u003cclient_name>] (optional, may be absent)\n\n There is no num_args field in the raw output. The client IP line\n (IPv4 or IPv6 with port) marks the end of each entry's arguments.\n \"\"\"\n entries: List[Dict[str, Any]] = []\n lines = [l.strip() for l in raw.strip().splitlines() if l.strip()]\n if not lines:\n return entries\n\n i = 0\n while i \u003c len(lines):\n try:\n entry_id = int(lines[i])\n except (ValueError, IndexError):\n i += 1\n continue\n\n if i + 3 >= len(lines):\n break\n\n try:\n timestamp = int(lines[i + 1])\n duration_us = int(lines[i + 2])\n except (ValueError, IndexError):\n i += 1\n continue\n\n # lines[i+3] is the command name; scan forward for client IP\n cmd_start = i + 3\n client_ip_pos = None\n for k in range(cmd_start, min(cmd_start + 30, len(lines))):\n if _CLIENT_IP_RE.match(lines[k]):\n client_ip_pos = k\n break\n\n if client_ip_pos is not None:\n cmd_parts = lines[cmd_start:client_ip_pos]\n # Advance past client IP and optional client name\n next_i = client_ip_pos + 1\n if next_i \u003c len(lines):\n try:\n int(lines[next_i])\n except ValueError:\n next_i += 1 # skip client name\n else:\n # No client IP found — take command + first arg only and advance\n cmd_parts = lines[cmd_start:cmd_start + 2]\n next_i = cmd_start + 2\n\n command = \" \".join(cmd_parts) if cmd_parts else \"unknown\"\n if len(command) > 120:\n command = command[:117] + \"...\"\n\n entries.append({\n \"id\": entry_id,\n \"timestamp_unix\": timestamp,\n \"duration_us\": duration_us,\n \"command\": command,\n })\n\n i = next_i\n\n return entries\n\n\ndef parse_bigkeys(raw: str) -> List[Dict[str, Any]]:\n \"\"\"Parse redis-cli --bigkeys output into structured entries.\n\n Looks for lines like:\n Biggest string found \"cache:render:page/dashboard\" has 2145832 bytes\n Biggest hash found \"user:sessions\" has 14291 fields\n Biggest list found \"queue:notifications\" has 8402 items\n Biggest set found \"tags:all\" has 291 members\n Biggest zset found \"leaderboard:global\" has 10042 members\n Biggest stream found \"events:main\" has 5012 entries\n \"\"\"\n entries: List[Dict[str, Any]] = []\n # Match: Biggest \u003ctype> found \"\u003ckey>\" has \u003ccount> \u003cunit>\n # Redis 8+ uses double quotes; older versions used single quotes\n pattern = re.compile(\n r'Biggest\\s+(\\w+)\\s+found\\s+[\"\\']([^\"\\']+)[\"\\']\\s+has\\s+([\\d,]+)\\s+(\\w+)',\n re.IGNORECASE,\n )\n for line in raw.splitlines():\n m = pattern.search(line)\n if m:\n key_type = m.group(1).lower()\n key_name = m.group(2)\n size_str = m.group(3).replace(\",\", \"\")\n unit = m.group(4).lower()\n size = _safe_int(size_str)\n\n # Format size for display\n if unit == \"bytes\":\n detail = _format_bytes_human(size)\n else:\n detail = f\"{size:,} {unit}\"\n\n entries.append({\n \"type\": key_type,\n \"key\": key_name,\n \"size_or_count\": size,\n \"detail\": detail,\n })\n return entries\n\n\n# ---------------------------------------------------------------------------\n# Formatting helpers\n# ---------------------------------------------------------------------------\n\ndef _format_number(n: int) -> str:\n \"\"\"Format a large number with K/M/B suffixes.\"\"\"\n if n >= 1_000_000_000:\n return f\"{n / 1_000_000_000:.1f}B\"\n if n >= 1_000_000:\n return f\"{n / 1_000_000:.1f}M\"\n if n >= 1_000:\n return f\"{n / 1_000:.1f}K\"\n return f\"{n:,}\"\n\n\ndef _format_duration(seconds: int) -> str:\n \"\"\"Format a duration in seconds to a human-readable relative string.\"\"\"\n if seconds \u003c= 0:\n return \"N/A\"\n if seconds \u003c 60:\n return f\"{seconds}s ago\"\n if seconds \u003c 3600:\n return f\"{seconds // 60}m ago\"\n if seconds \u003c 86400:\n return f\"{seconds // 3600}h ago\"\n return f\"{seconds // 86400}d ago\"\n\n\ndef _format_ttl(ms: int) -> str:\n \"\"\"Format average TTL in milliseconds to a human-readable string.\"\"\"\n if ms \u003c= 0:\n return \"none\"\n seconds = ms // 1000\n if seconds \u003c 60:\n return f\"{seconds}s\"\n if seconds \u003c 3600:\n return f\"{seconds // 60}m\"\n if seconds \u003c 86400:\n return f\"{seconds // 3600}h\"\n return f\"{seconds // 86400}d\"\n\n\ndef _format_usec(usec: float) -> str:\n \"\"\"Format microseconds to a human-readable string.\"\"\"\n if usec \u003c 1000:\n return f\"{usec:.1f}us\"\n if usec \u003c 1_000_000:\n return f\"{usec / 1000:.1f}ms\"\n return f\"{usec / 1_000_000:.2f}s\"\n\n\ndef _format_total_time(usec: int) -> str:\n \"\"\"Format total microseconds to a readable time string.\"\"\"\n seconds = usec / 1_000_000\n if seconds \u003c 1:\n return f\"{usec / 1000:.1f}ms\"\n if seconds \u003c 60:\n return f\"{seconds:.1f}s\"\n if seconds \u003c 3600:\n return f\"{seconds / 60:.1f}m\"\n return f\"{seconds / 3600:.1f}h\"\n\n\ndef _format_bytes_human(nbytes: int) -> str:\n \"\"\"Format bytes into human-readable string.\"\"\"\n if nbytes \u003c= 0:\n return \"0\"\n for unit in [\"B\", \"K\", \"M\", \"G\", \"T\"]:\n if nbytes \u003c 1024:\n return f\"{nbytes:.1f}{unit}\" if nbytes != int(nbytes) else f\"{int(nbytes)}{unit}\"\n nbytes /= 1024\n return f\"{nbytes:.1f}P\"\n\n\n# ---------------------------------------------------------------------------\n# Recommendations engine\n# ---------------------------------------------------------------------------\n\ndef generate_recommendations(result: RedisAnalysisResult) -> List[Dict[str, str]]:\n \"\"\"Generate recommendations based on collected metrics.\"\"\"\n recs: List[Dict[str, str]] = []\n\n # Collection failures — surface critical issues when SSH/introspection failed\n if result.collection_status:\n failed = {k: v for k, v in result.collection_status.items() if v.get(\"status\") == \"failed\"}\n ssh_sources = {\"redis_info\", \"slowlog\", \"slowlog_entries\", \"big_keys\"}\n ssh_failed = {k: v for k, v in failed.items() if k in ssh_sources}\n if ssh_failed:\n sources = \", \".join(ssh_failed.keys())\n errors = \"; \".join(v.get(\"error\", \"unknown\") for v in ssh_failed.values())\n recs.append({\n \"severity\": \"critical\",\n \"category\": \"collection\",\n \"message\": f\"SSH introspection failed — unable to collect {sources}. \"\n f\"Error: {errors}. \"\n f\"Analysis is incomplete: memory fragmentation, cache hit rate, \"\n f\"keyspace stats, and persistence health could not be evaluated.\",\n })\n\n # Memory fragmentation\n if result.memory:\n frag = result.memory.get(\"mem_fragmentation_ratio\", 0)\n if frag > 1.5:\n recs.append({\n \"severity\": \"warning\",\n \"category\": \"memory\",\n \"message\": f\"High memory fragmentation ({frag:.2f}). Consider restarting Redis to defragment, or enable activedefrag.\",\n })\n\n # Cache hit rate\n if result.cache:\n hit_rate = result.cache.get(\"hit_rate\", 0)\n if hit_rate \u003c 80 and (result.cache.get(\"keyspace_hits\", 0) + result.cache.get(\"keyspace_misses\", 0)) > 0:\n recs.append({\n \"severity\": \"warning\",\n \"category\": \"cache\",\n \"message\": f\"Low cache hit rate ({hit_rate:.1f}%). Review key access patterns - many keys may be expired or evicted before use.\",\n })\n elif hit_rate \u003c 95 and hit_rate >= 80:\n recs.append({\n \"severity\": \"info\",\n \"category\": \"cache\",\n \"message\": f\"Cache hit rate at {hit_rate:.1f}% — could be improved. Check if working set fits in memory.\",\n })\n\n # Evicted keys\n if result.cache:\n evicted = result.cache.get(\"evicted_keys\", 0)\n if evicted > 0:\n recs.append({\n \"severity\": \"warning\",\n \"category\": \"memory\",\n \"message\": f\"Redis is evicting keys ({_format_number(evicted)} evicted). Increase maxmemory or reduce dataset size.\",\n })\n\n # Rejected connections\n if result.overview:\n rejected = result.overview.get(\"rejected_connections\", 0)\n if rejected > 0:\n recs.append({\n \"severity\": \"warning\",\n \"category\": \"connections\",\n \"message\": f\"Connections being rejected ({_format_number(rejected)}). Check maxclients setting.\",\n })\n\n # Blocked clients\n if result.overview:\n blocked = result.overview.get(\"blocked_clients\", 0)\n if blocked > 0:\n recs.append({\n \"severity\": \"info\",\n \"category\": \"connections\",\n \"message\": f\"Blocked clients detected ({blocked}). Check for blocking operations (BLPOP, BRPOP, etc.).\",\n })\n\n # maxmemory not set — on Railway this is expected; autoscaling handles growth\n\n # RDB save failure\n if result.persistence:\n rdb_status = result.persistence.get(\"rdb_last_bgsave_status\", \"\")\n if rdb_status and rdb_status != \"ok\":\n recs.append({\n \"severity\": \"critical\",\n \"category\": \"persistence\",\n \"message\": \"Last RDB save failed. Check disk space and permissions.\",\n })\n\n # Slow log — data-driven when entries are available\n if result.slowlog_entries:\n # Analyze the actual slow commands\n total_entries = len(result.slowlog_entries)\n cmd_counts: Dict[str, int] = {}\n total_duration = 0\n for entry in result.slowlog_entries:\n cmd = entry[\"command\"].split()[0] if entry[\"command\"] else \"unknown\"\n cmd_counts[cmd] = cmd_counts.get(cmd, 0) + 1\n total_duration += entry[\"duration_us\"]\n top_cmd = max(cmd_counts, key=cmd_counts.get) if cmd_counts else \"unknown\"\n top_count = cmd_counts.get(top_cmd, 0)\n avg_duration = total_duration / total_entries if total_entries > 0 else 0\n\n msg = (f\"Slow log contains {result.slowlog_len or total_entries} entries. \"\n f\"Of the {total_entries} most recent: {top_count} are {top_cmd} commands \"\n f\"averaging {_format_usec(avg_duration)}.\")\n if result.big_keys:\n big_key_types = \", \".join(f\"{bk['type']} ({bk['detail']})\" for bk in result.big_keys[:3])\n msg += f\" Largest keys: {big_key_types} — check if these correlate with slow commands.\"\n severity = \"warning\" if (result.slowlog_len or 0) > 100 else \"info\"\n recs.append({\"severity\": severity, \"category\": \"performance\", \"message\": msg})\n elif result.slowlog_len is not None and result.slowlog_len > 100:\n recs.append({\n \"severity\": \"warning\",\n \"category\": \"performance\",\n \"message\": f\"High number of slow log entries ({result.slowlog_len}). Slow log details could not be collected.\",\n })\n\n # Big keys — standalone recommendation when no slowlog correlation\n if result.big_keys and not result.slowlog_entries:\n big_key_summary = \"; \".join(f\"{bk['key']} ({bk['type']}: {bk['detail']})\" for bk in result.big_keys[:5])\n recs.append({\n \"severity\": \"info\",\n \"category\": \"performance\",\n \"message\": f\"Largest keys by type: {big_key_summary}. Large keys can cause latency spikes on read/delete operations.\",\n })\n\n return recs\n\n\n# ---------------------------------------------------------------------------\n# Report formatting\n# ---------------------------------------------------------------------------\n\ndef format_report(result: RedisAnalysisResult) -> str:\n \"\"\"Format the analysis result as a markdown report.\"\"\"\n lines: List[str] = []\n\n lines.append(f\"# Redis Analysis: {result.service}\")\n lines.append(f\"Timestamp: {result.timestamp}\")\n lines.append(f\"Deployment Status: {result.deployment_status}\")\n lines.append(\"\")\n\n # --- Overview ---\n if result.overview:\n o = result.overview\n lines.append(\"## Overview\")\n lines.append(\"| Metric | Value |\")\n lines.append(\"|--------|-------|\")\n lines.append(f\"| Version | {o.get('redis_version', 'N/A')} |\")\n lines.append(f\"| Uptime | {_format_uptime(o.get('uptime_in_seconds', 0))} |\")\n lines.append(f\"| Connected Clients | {o.get('connected_clients', 0):,} |\")\n lines.append(f\"| Blocked Clients | {o.get('blocked_clients', 0):,} |\")\n lines.append(f\"| Rejected Connections | {o.get('rejected_connections', 0):,} |\")\n lines.append(f\"| Total Keys | {result.total_keys:,} |\")\n lines.append(\"\")\n\n # --- Memory ---\n if result.memory:\n m = result.memory\n lines.append(\"## Memory\")\n lines.append(\"| Metric | Value | Status |\")\n lines.append(\"|--------|-------|--------|\")\n\n frag = m.get(\"mem_fragmentation_ratio\", 0)\n frag_status = \"OK\" if 1.0 \u003c= frag \u003c= 1.5 else (\"HIGH\" if frag > 1.5 else \"LOW\")\n\n lines.append(f\"| Used Memory | {m.get('used_memory_human', 'N/A')} | |\")\n lines.append(f\"| RSS Memory | {m.get('used_memory_rss_human', 'N/A')} | |\")\n lines.append(f\"| Peak Memory | {m.get('used_memory_peak_human', 'N/A')} | |\")\n lines.append(f\"| Fragmentation Ratio | {frag:.2f} | {frag_status} |\")\n\n maxmem = m.get(\"maxmemory\", 0)\n if maxmem > 0:\n lines.append(f\"| Max Memory | {m.get('maxmemory_human', _format_bytes_human(maxmem))} | |\")\n else:\n lines.append(\"| Max Memory | Unlimited | |\")\n\n lines.append(f\"| Eviction Policy | {m.get('maxmemory_policy', 'N/A')} | |\")\n lines.append(\"\")\n\n # --- Throughput ---\n if result.throughput:\n t = result.throughput\n lines.append(\"## Throughput\")\n lines.append(\"| Metric | Value |\")\n lines.append(\"|--------|-------|\")\n lines.append(f\"| Ops/sec | {t.get('instantaneous_ops_per_sec', 0):,} |\")\n lines.append(f\"| Total Commands | {_format_number(t.get('total_commands_processed', 0))} |\")\n lines.append(f\"| Total Connections | {_format_number(t.get('total_connections_received', 0))} |\")\n if result.slowlog_len is not None:\n lines.append(f\"| Slow Log Entries | {result.slowlog_len:,} |\")\n lines.append(\"\")\n\n # --- Cache Performance ---\n if result.cache:\n c = result.cache\n hit_rate = c.get(\"hit_rate\", 0)\n hit_status = \"OK\" if hit_rate >= 95 else (\"WARN\" if hit_rate >= 80 else \"LOW\")\n evicted = c.get(\"evicted_keys\", 0)\n evict_status = \"OK\" if evicted == 0 else \"WARN\"\n\n lines.append(\"## Cache Performance\")\n lines.append(\"| Metric | Value | Status |\")\n lines.append(\"|--------|-------|--------|\")\n lines.append(f\"| Hit Rate | {hit_rate:.1f}% | {hit_status} |\")\n lines.append(f\"| Hits | {_format_number(c.get('keyspace_hits', 0))} | |\")\n lines.append(f\"| Misses | {_format_number(c.get('keyspace_misses', 0))} | |\")\n lines.append(f\"| Expired Keys | {_format_number(c.get('expired_keys', 0))} | |\")\n lines.append(f\"| Evicted Keys | {_format_number(evicted)} | {evict_status} |\")\n lines.append(\"\")\n\n # --- Persistence ---\n if result.persistence:\n p = result.persistence\n rdb_status = p.get(\"rdb_last_bgsave_status\", \"unknown\")\n rdb_status_display = \"OK\" if rdb_status == \"ok\" else \"FAIL\"\n\n lines.append(\"## Persistence\")\n lines.append(\"| Metric | Value | Status |\")\n lines.append(\"|--------|-------|--------|\")\n\n rdb_last_save = p.get(\"rdb_last_save_time\", 0)\n if rdb_last_save > 0:\n now_epoch = int(datetime.now(timezone.utc).timestamp())\n save_ago = now_epoch - rdb_last_save\n lines.append(f\"| RDB Last Save | {_format_duration(save_ago)} | |\")\n else:\n lines.append(\"| RDB Last Save | never | |\")\n\n lines.append(f\"| RDB Status | {rdb_status} | {rdb_status_display} |\")\n lines.append(f\"| AOF Enabled | {'Yes' if p.get('aof_enabled') else 'No'} | |\")\n\n if p.get(\"aof_enabled\"):\n aof_status = p.get(\"aof_last_rewrite_status\", \"unknown\")\n aof_display = \"OK\" if aof_status == \"ok\" else aof_status\n lines.append(f\"| AOF Rewrite Status | {aof_status} | {aof_display} |\")\n\n lines.append(\"\")\n\n # --- Command Stats ---\n if result.command_stats:\n top_n = result.command_stats[:20]\n lines.append(\"## Command Stats (top 20)\")\n lines.append(\"| Command | Calls | Avg Latency | Total Time |\")\n lines.append(\"|---------|-------|-------------|------------|\")\n for cs in top_n:\n lines.append(\n f\"| {cs['command']} \"\n f\"| {_format_number(cs['calls'])} \"\n f\"| {_format_usec(cs['usec_per_call'])} \"\n f\"| {_format_total_time(cs['usec'])} |\"\n )\n lines.append(\"\")\n\n # --- Slow Log Entries ---\n if result.slowlog_entries:\n lines.append(\"## Slow Log Entries (recent)\")\n lines.append(\"| # | Timestamp | Duration | Command |\")\n lines.append(\"|---|-----------|----------|---------|\")\n now_epoch = int(datetime.now(timezone.utc).timestamp())\n for entry in result.slowlog_entries:\n age = now_epoch - entry[\"timestamp_unix\"]\n lines.append(\n f\"| {entry['id']} \"\n f\"| {_format_duration(age)} \"\n f\"| {_format_usec(entry['duration_us'])} \"\n f\"| {entry['command']} |\"\n )\n lines.append(\"\")\n\n # --- Biggest Keys ---\n if result.big_keys:\n lines.append(\"## Biggest Keys\")\n lines.append(\"| Type | Key | Size/Count |\")\n lines.append(\"|------|-----|------------|\")\n for bk in result.big_keys:\n lines.append(\n f\"| {bk['type']} \"\n f\"| {bk['key']} \"\n f\"| {bk['detail']} |\"\n )\n lines.append(\"\")\n\n # --- Keyspace ---\n if result.keyspace:\n lines.append(\"## Keyspace\")\n lines.append(\"| Database | Keys | Expires | Avg TTL |\")\n lines.append(\"|----------|------|---------|---------|\")\n for db in result.keyspace:\n lines.append(\n f\"| {db['db']} \"\n f\"| {db['keys']:,} \"\n f\"| {db['expires']:,} \"\n f\"| {_format_ttl(db['avg_ttl'])} |\"\n )\n lines.append(\"\")\n\n # --- Infrastructure Metrics ---\n if result.metrics_history:\n windows = result.metrics_history.get(\"windows\", {})\n for window_label, window_data in windows.items():\n mh = window_data.get(\"metrics\", {})\n if not mh:\n continue\n lines.append(f\"## Infrastructure Metrics ({window_label})\")\n lines.append(\"| Metric | Current | Min | Max | Avg | Trend |\")\n lines.append(\"|--------|---------|-----|-----|-----|-------|\")\n for key in [\"cpu\", \"memory\", \"disk\", \"network_rx\", \"network_tx\"]:\n if key in mh:\n entry = mh[key]\n trend = entry.get(\"trend\", {})\n trend_str = trend.get(\"direction\", \"N/A\")\n change = trend.get(\"change_pct\", 0)\n if change != 0:\n trend_str += f\" ({change:+.1f}%)\"\n lines.append(\n f\"| {key.replace('_', ' ').title()} \"\n f\"| {entry['current']}{entry['unit']} \"\n f\"| {entry['min']}{entry['unit']} \"\n f\"| {entry['max']}{entry['unit']} \"\n f\"| {entry['avg']}{entry['unit']} \"\n f\"| {trend_str} |\"\n )\n lines.append(\"\")\n\n # --- Collection Status ---\n if result.collection_status:\n failed = {k: v for k, v in result.collection_status.items() if v.get(\"status\") == \"failed\"}\n if failed:\n lines.append(\"## Collection Issues\")\n for source, status in failed.items():\n lines.append(f\"- **{source}**: {status.get('error', 'unknown error')}\")\n lines.append(\"\")\n\n # --- Recommendations ---\n if result.recommendations:\n lines.append(\"## Recommendations\")\n for rec in result.recommendations:\n severity = rec.get(\"severity\", \"info\").upper()\n lines.append(f\"- [{severity}] {rec['message']}\")\n lines.append(\"\")\n\n return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# Main analysis function\n# ---------------------------------------------------------------------------\n\ndef analyze_redis(service: str, timeout: int = 300, quiet: bool = False,\n skip_logs: bool = False,\n metrics_hours: int = 168,\n project_id: Optional[str] = None,\n environment_id: Optional[str] = None,\n service_id: Optional[str] = None) -> RedisAnalysisResult:\n \"\"\"Run complete Redis analysis with maximum data collection.\n\n Collects Redis INFO ALL, SLOWLOG LEN, SLOWLOG GET 20, --bigkeys,\n Railway metrics, and logs in parallel where possible.\n\n Args:\n skip_logs: Skip log fetching for faster analysis\n metrics_hours: Hours of metrics history to fetch (default: 168, max: 168)\n project_id: Project ID (bypasses railway link config)\n environment_id: Environment ID (bypasses railway link config)\n service_id: Service ID (bypasses railway link config)\n \"\"\"\n if not quiet:\n print(f\"Analyzing redis database: {service}\", file=sys.stderr)\n\n result = RedisAnalysisResult(\n service=service,\n db_type=\"redis\",\n timestamp=datetime.now(timezone.utc).isoformat(),\n )\n\n # === FAST CONTEXT LOADING ===\n if not quiet:\n print(\" [0/5] Getting Railway context...\", file=sys.stderr, flush=True)\n dal._progress_timer.start()\n\n if environment_id and service_id:\n dal._ctx = RailwayContext(project_id=project_id, environment_id=environment_id, service_id=service_id)\n if not quiet:\n print(f\" using explicit IDs (env={environment_id[:8]}..., svc={service_id[:8]}...)\", file=sys.stderr, flush=True)\n else:\n railway_status = get_railway_status()\n if railway_status:\n dal._ctx = RailwayContext(\n project_id=railway_status.get(\"projectId\"),\n environment_id=railway_status.get(\"environmentId\"),\n service_id=railway_status.get(\"serviceId\"),\n )\n environment_id = dal._ctx.environment_id\n service_id = dal._ctx.service_id\n\n # Get deployment status via API (~1s)\n progress(1, 5, \"Fetching deployment status...\", quiet)\n result.deployment_status = get_deployment_status(service, service_id=service_id)\n\n # === SSH PRE-CHECK WITH RETRY ===\n progress(2, 5, \"Testing SSH connectivity...\", quiet)\n ssh_available = False\n ssh_stderr = \"\"\n ssh_attempts = [30, 60, 90]\n for attempt, attempt_timeout in enumerate(ssh_attempts, 1):\n ssh_code, ssh_stdout, ssh_stderr = run_ssh_query(service, \"echo ok\", timeout=attempt_timeout)\n if ssh_code == 0 and \"ok\" in ssh_stdout:\n ssh_available = True\n if not quiet:\n for line in ssh_stderr.splitlines():\n if line.startswith(\"Using SSH key:\"):\n print(f\" {line}\", file=sys.stderr, flush=True)\n break\n break\n if not quiet:\n remaining = len(ssh_attempts) - attempt\n if remaining > 0:\n print(f\" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), retrying with {ssh_attempts[attempt]}s timeout...\", file=sys.stderr, flush=True)\n else:\n print(f\" SSH attempt {attempt}/{len(ssh_attempts)} failed ({ssh_stderr or 'no response'}), giving up\", file=sys.stderr, flush=True)\n\n # === PARALLEL EXECUTION ===\n progress(3, 5, \"Running analysis (Redis INFO, slowlog, bigkeys, metrics, logs in parallel)...\", quiet)\n\n def task_redis_info():\n \"\"\"Fetch Redis INFO ALL via SSH.\"\"\"\n if not ssh_available:\n return (\"failed\", f\"SSH not available: {ssh_stderr or 'connection failed'}\", \"\")\n command = 'timeout 30s redis-cli -h localhost -p 6379 -a \"$REDISPASSWORD\" --no-auth-warning --raw INFO ALL'\n code, stdout, stderr = run_ssh_query(service, command, timeout=45)\n if code == 0 and stdout.strip():\n return (\"ok\", \"\", stdout)\n return (\"failed\", stderr or \"empty response\", stdout)\n\n def task_slowlog():\n \"\"\"Fetch Redis SLOWLOG LEN via SSH.\"\"\"\n if not ssh_available:\n return (\"failed\", f\"SSH not available: {ssh_stderr or 'connection failed'}\", \"\")\n command = 'timeout 30s redis-cli -h localhost -p 6379 -a \"$REDISPASSWORD\" --no-auth-warning --raw SLOWLOG LEN'\n code, stdout, stderr = run_ssh_query(service, command, timeout=45)\n if code == 0 and stdout.strip():\n return (\"ok\", \"\", stdout.strip())\n return (\"failed\", stderr or \"empty response\", \"\")\n\n def task_slowlog_get():\n \"\"\"Fetch Redis SLOWLOG GET 20 via SSH for actual slow query details.\"\"\"\n if not ssh_available:\n return (\"failed\", f\"SSH not available: {ssh_stderr or 'connection failed'}\", \"\")\n command = 'timeout 30s redis-cli -h localhost -p 6379 -a \"$REDISPASSWORD\" --no-auth-warning --raw SLOWLOG GET 20'\n code, stdout, stderr = run_ssh_query(service, command, timeout=45)\n if code == 0 and stdout.strip():\n return (\"ok\", \"\", stdout.strip())\n return (\"failed\", stderr or \"empty response\", \"\")\n\n def task_bigkeys():\n \"\"\"Fetch redis-cli --bigkeys via SSH (SCAN-based, may take longer).\"\"\"\n if not ssh_available:\n return (\"failed\", f\"SSH not available: {ssh_stderr or 'connection failed'}\", \"\")\n command = 'timeout 60s redis-cli -h localhost -p 6379 -a \"$REDISPASSWORD\" --no-auth-warning --bigkeys'\n code, stdout, stderr = run_ssh_query(service, command, timeout=75)\n if code == 0 and stdout.strip():\n return (\"ok\", \"\", stdout.strip())\n return (\"failed\", stderr or \"empty response\", \"\")\n\n def task_metrics():\n \"\"\"Fetch all metrics (disk, CPU, memory) in one API call.\"\"\"\n if environment_id and service_id:\n return get_all_metrics_from_api(environment_id, service_id, hours=metrics_hours)\n return None\n\n def task_logs():\n \"\"\"Fetch recent logs via API (~3s).\"\"\"\n if skip_logs:\n return []\n return get_recent_logs(service, lines=LOG_LINES_DEFAULT,\n environment_id=environment_id,\n service_id=service_id)\n\n with ThreadPoolExecutor(max_workers=6) as executor:\n future_info = executor.submit(task_redis_info)\n future_slowlog = executor.submit(task_slowlog)\n future_slowlog_get = executor.submit(task_slowlog_get)\n future_bigkeys = executor.submit(task_bigkeys)\n future_metrics = executor.submit(task_metrics)\n future_logs = executor.submit(task_logs)\n\n # Collect results\n info_result = future_info.result()\n slowlog_result = future_slowlog.result()\n slowlog_get_result = future_slowlog_get.result()\n bigkeys_result = future_bigkeys.result()\n metrics_result = future_metrics.result()\n logs_result = future_logs.result()\n\n # === PROCESS RESULTS ===\n progress(4, 5, \"Processing results...\", quiet)\n\n # Redis INFO ALL\n info_status, info_error, info_raw = info_result\n if info_status == \"ok\" and info_raw:\n result.collection_status[\"redis_info\"] = {\"status\": \"ok\"}\n info = parse_redis_info(info_raw)\n\n result.overview = extract_overview(info)\n result.memory = extract_memory(info)\n result.throughput = extract_throughput(info)\n result.cache = extract_cache(info)\n result.persistence = extract_persistence(info)\n result.keyspace, result.total_keys = extract_keyspace(info)\n result.command_stats = extract_command_stats(info)\n else:\n result.collection_status[\"redis_info\"] = {\"status\": \"failed\", \"error\": info_error}\n result.errors.append(f\"Redis INFO failed: {info_error}\")\n\n # SLOWLOG LEN\n sl_status, sl_error, sl_raw = slowlog_result\n if sl_status == \"ok\" and sl_raw:\n result.collection_status[\"slowlog\"] = {\"status\": \"ok\"}\n result.slowlog_len = _safe_int(sl_raw)\n else:\n result.collection_status[\"slowlog\"] = {\"status\": \"failed\", \"error\": sl_error}\n\n # SLOWLOG GET 20\n slg_status, slg_error, slg_raw = slowlog_get_result\n if slg_status == \"ok\" and slg_raw:\n result.collection_status[\"slowlog_entries\"] = {\"status\": \"ok\"}\n result.slowlog_entries = parse_slowlog_get(slg_raw)\n else:\n result.collection_status[\"slowlog_entries\"] = {\"status\": \"failed\", \"error\": slg_error}\n\n # Big keys\n bk_status, bk_error, bk_raw = bigkeys_result\n if bk_status == \"ok\" and bk_raw:\n result.collection_status[\"big_keys\"] = {\"status\": \"ok\"}\n result.big_keys = parse_bigkeys(bk_raw)\n else:\n result.collection_status[\"big_keys\"] = {\"status\": \"failed\", \"error\": bk_error}\n\n # Metrics\n if metrics_result:\n result.collection_status[\"metrics\"] = {\"status\": \"ok\"}\n result.metrics_history = metrics_result.get(\"metrics_history\")\n else:\n result.collection_status[\"metrics\"] = {\"status\": \"failed\", \"error\": \"no metrics returned\"}\n\n # Logs\n if logs_result:\n result.collection_status[\"logs\"] = {\"status\": \"ok\", \"lines\": len(logs_result)}\n result.recent_logs = logs_result\n else:\n result.collection_status[\"logs\"] = {\"status\": \"failed\", \"error\": \"no logs returned\"}\n\n # === RECOMMENDATIONS ===\n progress(5, 5, \"Generating recommendations...\", quiet)\n result.recommendations = generate_recommendations(result)\n\n if not quiet:\n elapsed = dal._progress_timer.step_elapsed()\n if elapsed:\n print(f\" done{elapsed}\", file=sys.stderr, flush=True)\n print(f\" Analysis complete{dal._progress_timer.total_elapsed()}\", file=sys.stderr, flush=True)\n\n return result\n\n\n# ---------------------------------------------------------------------------\n# Single-step debugging\n# ---------------------------------------------------------------------------\n\ndef run_single_step(args) -> int:\n \"\"\"Run a single collection step for debugging.\"\"\"\n service = args.service\n _init_context(args)\n environment_id = dal._ctx.environment_id\n service_id = dal._ctx.service_id\n\n if args.step == \"ssh-test\":\n print(f\"Testing SSH to service: {service}\", file=sys.stderr)\n code, stdout, stderr = run_ssh_query(service, \"echo ok\", timeout=45)\n print(f\"Exit code: {code}\")\n print(f\"Stdout: {stdout.strip()}\")\n if stderr:\n print(f\"Stderr: {stderr.strip()}\")\n return 0 if (code == 0 and \"ok\" in stdout) else 1\n\n elif args.step == \"query\":\n print(f\"Running Redis INFO ALL on: {service}\", file=sys.stderr)\n command = 'timeout 30s redis-cli -h localhost -p 6379 -a \"$REDISPASSWORD\" --no-auth-warning --raw INFO ALL'\n code, stdout, stderr = run_ssh_query(service, command, timeout=45)\n print(f\"Exit code: {code}\")\n if code == 0 and stdout:\n info = parse_redis_info(stdout)\n print(json.dumps(info, indent=2))\n else:\n print(f\"Error: {stderr or stdout}\")\n return code\n\n elif args.step == \"logs\":\n print(f\"Fetching logs for: {service}\", file=sys.stderr)\n logs = get_recent_logs(service, lines=LOG_LINES_DEFAULT,\n environment_id=environment_id,\n service_id=service_id)\n print(f\"Lines fetched: {len(logs)}\")\n for line in logs:\n print(line)\n return 0\n\n elif args.step == \"metrics\":\n print(f\"Fetching metrics for: {service}\", file=sys.stderr)\n if environment_id and service_id:\n metrics = get_all_metrics_from_api(environment_id, service_id)\n if metrics:\n print(json.dumps(metrics, indent=2))\n else:\n print(\"No metrics returned\", file=sys.stderr)\n return 1\n else:\n print(\"No environment_id or service_id available\", file=sys.stderr)\n return 1\n return 0\n\n else:\n print(f\"Unknown step: {args.step}\", file=sys.stderr)\n return 1\n\n\n# ---------------------------------------------------------------------------\n# CLI entry point\n# ---------------------------------------------------------------------------\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Redis analysis for Railway services.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n )\n\n parser.add_argument(\"--service\", required=True, help=\"Service name\")\n parser.add_argument(\"--json\", action=\"store_true\",\n help=\"Output as JSON\")\n parser.add_argument(\"--timeout\", type=int, default=300,\n help=\"Timeout in seconds for analysis (default: 300)\")\n parser.add_argument(\"--quiet\", \"-q\", action=\"store_true\",\n help=\"Suppress progress messages\")\n parser.add_argument(\"--skip-logs\", action=\"store_true\",\n help=\"Skip log fetching for faster analysis\")\n parser.add_argument(\"--metrics-hours\", type=int, default=168,\n help=\"Hours of metrics history to fetch (default: 168, max: 168)\")\n parser.add_argument(\"--step\", choices=[\"ssh-test\", \"query\", \"logs\", \"metrics\"],\n help=\"Run a single collection step for debugging\")\n parser.add_argument(\"--project-id\", help=\"Project ID (bypasses railway link)\")\n parser.add_argument(\"--environment-id\", help=\"Environment ID (bypasses railway link)\")\n parser.add_argument(\"--service-id\", help=\"Service ID (bypasses railway link)\")\n\n args = parser.parse_args()\n\n # Single-step debugging mode\n if args.step:\n return run_single_step(args)\n\n # Run analysis\n result = analyze_redis(args.service, timeout=args.timeout, quiet=args.quiet,\n skip_logs=args.skip_logs,\n metrics_hours=min(args.metrics_hours, 168),\n project_id=args.project_id,\n environment_id=args.environment_id,\n service_id=args.service_id)\n\n # Output\n if args.json:\n print(json.dumps(asdict(result), indent=2))\n else:\n print(format_report(result))\n\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":43254,"content_sha256":"42a24cef643539a6dc5815d28e84d20dfd6439015d0f086dfd7aafc4786a71cc"},{"filename":"scripts/dal.py","content":"#!/usr/bin/env python3\n\"\"\"Shared Railway infrastructure helpers for database analysis scripts.\"\"\"\n\nimport json\nimport os\nimport subprocess\nimport sys\nfrom datetime import datetime, timezone\nfrom typing import Any, Dict, List, Optional, Tuple\nfrom dataclasses import dataclass\n\nLOG_LINES_DEFAULT = 1000 # Number of log lines to fetch via API\n\n\nclass ProgressTimer:\n \"\"\"Track elapsed time for progress messages.\"\"\"\n def __init__(self):\n self.start_time = None\n self.step_start_time = None\n\n def start(self):\n \"\"\"Start the overall timer.\"\"\"\n self.start_time = datetime.now()\n self.step_start_time = self.start_time\n\n def step_elapsed(self) -> str:\n \"\"\"Get elapsed time since last step, then reset step timer.\"\"\"\n if self.step_start_time is None:\n return \"\"\n now = datetime.now()\n elapsed = (now - self.step_start_time).total_seconds()\n self.step_start_time = now\n if elapsed \u003c 0.1:\n return \"\"\n return f\" ({elapsed:.1f}s)\"\n\n def total_elapsed(self) -> str:\n \"\"\"Get total elapsed time.\"\"\"\n if self.start_time is None:\n return \"\"\n elapsed = (datetime.now() - self.start_time).total_seconds()\n return f\" (total: {elapsed:.1f}s)\"\n\n\n# Global timer instance\n_progress_timer = ProgressTimer()\n\n\n@dataclass\nclass RailwayContext:\n \"\"\"Explicit Railway IDs that bypass railway link.\"\"\"\n project_id: Optional[str] = None\n environment_id: Optional[str] = None\n service_id: Optional[str] = None\n\n def ssh_flags(self) -> List[str]:\n \"\"\"Return CLI flags for railway ssh.\"\"\"\n flags = [\"--native\"]\n if self.project_id:\n flags.extend([\"--project\", self.project_id])\n if self.environment_id:\n flags.extend([\"--environment\", self.environment_id])\n if self.service_id:\n flags.extend([\"--service\", self.service_id])\n return flags\n\n def logs_flags(self) -> List[str]:\n \"\"\"Return CLI flags for railway logs.\"\"\"\n flags = []\n if self.environment_id:\n flags.extend([\"--environment\", self.environment_id])\n return flags\n\n\n# Global context — set once at startup, used by all CLI calls\n_ctx = RailwayContext()\n\n\ndef _init_context(args) -> None:\n \"\"\"Initialize global context from CLI args or railway config.\"\"\"\n global _ctx\n if args.environment_id and args.service_id:\n _ctx = RailwayContext(\n project_id=getattr(args, 'project_id', None),\n environment_id=args.environment_id,\n service_id=args.service_id,\n )\n else:\n railway_status = get_railway_status()\n if railway_status:\n _ctx = RailwayContext(\n project_id=railway_status.get(\"projectId\"),\n environment_id=railway_status.get(\"environmentId\"),\n service_id=railway_status.get(\"serviceId\"),\n )\n\n\ndef progress(step: int, total: int, message: str, quiet: bool = False):\n \"\"\"Print progress message to stderr with elapsed time.\"\"\"\n if not quiet:\n # Show elapsed time from previous step (before current step message)\n elapsed = _progress_timer.step_elapsed()\n if elapsed:\n print(f\" done{elapsed}\", file=sys.stderr, flush=True)\n print(f\" [{step}/{total}] {message}\", file=sys.stderr, flush=True)\n\n\ndef run_railway_command(args: List[str], timeout: int = 30) -> Tuple[int, str, str]:\n \"\"\"Run a railway CLI command and return (returncode, stdout, stderr).\"\"\"\n try:\n result = subprocess.run(\n [\"railway\"] + args,\n capture_output=True,\n text=True,\n timeout=timeout\n )\n return result.returncode, result.stdout, result.stderr\n except subprocess.TimeoutExpired:\n return 124, \"\", \"Command timed out\"\n except FileNotFoundError:\n return 127, \"\", \"railway CLI not found\"\n\n\ndef _cli_fatal_error(returncode: int, stderr: str) -> Optional[str]:\n \"\"\"Return a friendly error string if the CLI itself is broken, else None.\n\n These errors are unrecoverable — retrying won't help.\n \"\"\"\n if returncode == 127 or \"railway CLI not found\" in stderr:\n return (\n \"Railway CLI not found. \"\n \"Install it with: npm i -g @railway/cli \"\n \"or brew install railway\"\n )\n lower = stderr.lower()\n if \"unknown flag\" in lower or \"flag provided but not defined\" in lower:\n return (\n \"Railway CLI is outdated — the --native SSH flag is not supported. \"\n \"Update it with: npm i -g @railway/cli@latest \"\n \"or brew upgrade railway\"\n )\n return None\n\n\ndef run_ssh_query(service: str, command: str, timeout: int = 60,\n max_attempts: int = 3) -> Tuple[int, str, str]:\n \"\"\"Run a command via railway ssh, retrying up to max_attempts times.\n\n Passes the command as a single argument after '--'. Railway ssh\n interprets it through a shell on the remote end, so pipes, env vars,\n and redirects all work without an explicit sh -c wrapper.\n\n Retries on non-zero exit code or empty stdout (covers transient errors\n like 'exec request failed on channel 0'). Never retries when the CLI\n itself is missing or outdated — those errors are unrecoverable.\n \"\"\"\n flags = _ctx.ssh_flags()\n # Only pass --service \u003cname> if context didn't already provide --service \u003cid>\n if not _ctx.service_id:\n flags += [\"--service\", service]\n args = [\"ssh\"] + flags + [\"--\", command]\n last_code, last_stdout, last_stderr = 1, \"\", \"\"\n for attempt in range(1, max_attempts + 1):\n last_code, last_stdout, last_stderr = run_railway_command(args, timeout)\n if last_code == 0 and last_stdout.strip():\n return last_code, last_stdout, last_stderr\n fatal = _cli_fatal_error(last_code, last_stderr)\n if fatal:\n return last_code, last_stdout, fatal\n if attempt \u003c max_attempts:\n print(\n f\" SSH attempt {attempt}/{max_attempts} failed \"\n f\"({last_stderr.strip() or 'empty response'}), retrying...\",\n file=sys.stderr, flush=True,\n )\n return last_code, last_stdout, last_stderr\n\n\ndef run_psql_query(service: str, query: str, timeout: int = 60) -> Tuple[int, str]:\n \"\"\"Run a psql query via railway ssh and return (returncode, output).\n\n Normalizes query whitespace and suppresses psql warnings (e.g. collation\n version mismatch) that would otherwise pollute stdout.\n \"\"\"\n query = \" \".join(query.split())\n command = f'''PAGER='' psql $DATABASE_URL -P pager=off -t -A -c \"{query}\" 2>/dev/null'''\n code, stdout, stderr = run_ssh_query(service, command, timeout)\n if code != 0:\n return code, stderr or stdout\n return 0, stdout\n\n\ndef get_railway_status() -> Optional[Dict[str, Any]]:\n \"\"\"Get environment and service IDs from Railway config file.\n\n Reads directly from ~/.railway/config.json instead of calling CLI (~15s saved).\n \"\"\"\n config_path = os.path.expanduser(\"~/.railway/config.json\")\n if not os.path.exists(config_path):\n return None\n\n try:\n with open(config_path, \"r\") as f:\n config = json.load(f)\n\n # Get linked project for current directory\n cwd = os.getcwd()\n projects = config.get(\"projects\", {})\n\n # Find project config for current directory or parent\n project_config = None\n check_path = cwd\n while check_path != \"/\":\n if check_path in projects:\n project_config = projects[check_path]\n break\n check_path = os.path.dirname(check_path)\n\n if not project_config:\n return None\n\n return {\n \"projectId\": project_config.get(\"project\"),\n \"environmentId\": project_config.get(\"environment\"),\n \"serviceId\": project_config.get(\"service\"),\n \"serviceName\": project_config.get(\"name\", \"\"),\n }\n except (json.JSONDecodeError, IOError):\n return None\n\n\ndef get_deployment_status(service: str, service_id: Optional[str] = None) -> str:\n \"\"\"Get deployment status for service.\n\n Uses direct API call if service_id provided (~1s), falls back to CLI (~15s).\n \"\"\"\n # Fast path: use API directly if we have service_id\n if service_id:\n script_dir = os.path.dirname(os.path.abspath(__file__))\n api_script = os.path.join(script_dir, \"railway-api.sh\")\n\n if os.path.exists(api_script):\n query = '''query svc($id: String!) {\n service(id: $id) {\n deployments(first: 1) {\n edges { node { status } }\n }\n }\n }'''\n try:\n result = subprocess.run(\n [api_script, query, json.dumps({\"id\": service_id})],\n capture_output=True,\n text=True,\n timeout=10\n )\n if result.returncode == 0:\n data = json.loads(result.stdout)\n edges = data.get(\"data\", {}).get(\"service\", {}).get(\"deployments\", {}).get(\"edges\", [])\n if edges:\n return edges[0].get(\"node\", {}).get(\"status\", \"UNKNOWN\")\n except (subprocess.TimeoutExpired, json.JSONDecodeError):\n pass\n\n # Fallback: use CLI (slow, ~15s)\n code, stdout, stderr = run_railway_command(\n [\"service\", \"status\", \"--service\", service, \"--json\"]\n )\n if code != 0:\n return \"UNKNOWN\"\n try:\n data = json.loads(stdout)\n status = data.get(\"status\", \"UNKNOWN\")\n if data.get(\"stopped\"):\n return f\"{status} (stopped)\"\n return status\n except json.JSONDecodeError:\n return \"UNKNOWN\"\n\n\ndef get_all_metrics_from_api(environment_id: str, service_id: str, hours: int = 24) -> Optional[Dict[str, Any]]:\n \"\"\"Get disk, CPU, memory, and network usage from Railway metrics API.\n\n Fetches time-series data and computes trend analysis including\n min/max/avg, spike detection, and directional trends.\n\n Args:\n hours: Hours of history to fetch (default: 24, max: 168)\n \"\"\"\n from datetime import timedelta\n\n start_date = (datetime.now(timezone.utc) - timedelta(hours=hours)).isoformat()\n script_dir = os.path.dirname(os.path.abspath(__file__))\n api_script = os.path.join(script_dir, \"railway-api.sh\")\n\n if not os.path.exists(api_script):\n return None\n\n query = '''query metrics($environmentId: String!, $serviceId: String, $startDate: DateTime!, $measurements: [MetricMeasurement!]!) {\n metrics(environmentId: $environmentId, serviceId: $serviceId, startDate: $startDate, measurements: $measurements) {\n measurement values { ts value }\n }\n }'''\n\n variables = json.dumps({\n \"environmentId\": environment_id,\n \"serviceId\": service_id,\n \"startDate\": start_date,\n \"measurements\": [\n \"DISK_USAGE_GB\", \"CPU_USAGE\", \"MEMORY_USAGE_GB\",\n \"MEMORY_LIMIT_GB\", \"CPU_LIMIT\",\n \"NETWORK_RX_GB\", \"NETWORK_TX_GB\",\n ]\n })\n\n try:\n result = subprocess.run(\n [api_script, query, variables],\n capture_output=True,\n text=True,\n timeout=30\n )\n if result.returncode != 0:\n return None\n\n data = json.loads(result.stdout)\n metrics = data.get(\"data\", {}).get(\"metrics\", [])\n\n combined = {\"disk_usage\": None, \"cpu_memory\": {}, \"metrics_history\": None}\n\n # Raw time series keyed by measurement name\n raw_series: Dict[str, List[Dict[str, Any]]] = {}\n\n for metric in metrics:\n measurement = metric.get(\"measurement\")\n values = metric.get(\"values\", [])\n if values:\n raw_series[measurement] = values\n latest = values[-1].get(\"value\", 0)\n if measurement == \"DISK_USAGE_GB\":\n combined[\"disk_usage\"] = {\n \"used_gb\": round(latest, 2),\n \"used\": f\"{round(latest, 1)} GB\"\n }\n elif measurement == \"CPU_USAGE\":\n combined[\"cpu_memory\"][\"cpu_percent\"] = round(latest, 1)\n elif measurement == \"MEMORY_USAGE_GB\":\n combined[\"cpu_memory\"][\"memory_gb\"] = round(latest, 2)\n elif measurement == \"MEMORY_LIMIT_GB\":\n combined[\"cpu_memory\"][\"memory_limit_gb\"] = round(latest, 2)\n elif measurement == \"CPU_LIMIT\":\n combined[\"cpu_memory\"][\"cpu_limit\"] = round(latest, 1)\n\n if not combined[\"cpu_memory\"]:\n combined[\"cpu_memory\"] = None\n\n # Build time-series history with trend analysis\n if raw_series:\n combined[\"metrics_history\"] = _build_metrics_history(raw_series, hours=hours)\n\n return combined\n except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):\n pass\n\n return None\n\n\ndef _analyze_window(values: List[Dict[str, Any]], nums: List[float], d: int,\n unit: str) -> Dict[str, Any]:\n \"\"\"Analyze a single time window of metric data.\n\n Returns summary stats, trend, spike detection, and downsampled series.\n \"\"\"\n if not nums:\n return {}\n\n avg_val = sum(nums) / len(nums)\n min_val = min(nums)\n max_val = max(nums)\n\n entry: Dict[str, Any] = {\n \"unit\": unit,\n \"current\": round(nums[-1], d),\n \"min\": round(min_val, d),\n \"max\": round(max_val, d),\n \"avg\": round(avg_val, d),\n \"samples\": len(nums),\n }\n\n # Trend: compare first quarter avg to last quarter avg\n q_size = max(len(nums) // 4, 1)\n first_q = nums[:q_size]\n last_q = nums[-q_size:]\n first_avg = sum(first_q) / len(first_q)\n last_avg = sum(last_q) / len(last_q)\n\n if first_avg > 0:\n change_pct = round(((last_avg - first_avg) / first_avg) * 100, 1)\n elif last_avg > 0:\n change_pct = 100.0\n else:\n change_pct = 0.0\n\n if change_pct > 10:\n trend_dir = \"increasing\"\n elif change_pct \u003c -10:\n trend_dir = \"decreasing\"\n else:\n trend_dir = \"stable\"\n\n entry[\"trend\"] = {\n \"direction\": trend_dir,\n \"change_pct\": change_pct,\n \"first_quarter_avg\": round(first_avg, d),\n \"last_quarter_avg\": round(last_avg, d),\n }\n\n # Spike detection\n if len(nums) >= 10:\n variance = sum((x - avg_val) ** 2 for x in nums) / len(nums)\n stddev = variance ** 0.5\n threshold = avg_val + 2 * stddev\n if stddev > 0 and threshold > 0:\n spikes = []\n for v in values:\n val = v.get(\"value\")\n if val is not None and val > threshold:\n spikes.append({\"ts\": v[\"ts\"], \"value\": round(val, d)})\n if spikes:\n entry[\"spikes\"] = {\n \"count\": len(spikes),\n \"threshold\": round(threshold, d),\n \"peaks\": spikes[:10],\n }\n\n # Compact time series: downsample to ~48 points\n series_points = []\n for v in values:\n ts = v.get(\"ts\")\n val = v.get(\"value\")\n if ts is not None and val is not None:\n series_points.append({\"ts\": ts, \"value\": round(val, d)})\n\n if len(series_points) > 48:\n step = len(series_points) / 48\n downsampled = []\n for i in range(48):\n idx = int(i * step)\n downsampled.append(series_points[idx])\n downsampled.append(series_points[-1])\n entry[\"series\"] = downsampled\n else:\n entry[\"series\"] = series_points\n\n return entry\n\n\ndef _build_metrics_history(raw_series: Dict[str, List[Dict[str, Any]]], hours: int = 168) -> Dict[str, Any]:\n \"\"\"Build multi-window time-series history with trend analysis.\n\n Always produces a full-window analysis. If the window is > 24h, also\n produces a 24h short-window analysis from the tail of the data so the\n LLM can compare long-term vs short-term trends.\n \"\"\"\n from datetime import timedelta\n\n metric_info = {\n \"CPU_USAGE\": {\"name\": \"cpu\", \"unit\": \"vCPU\", \"decimals\": 2},\n \"MEMORY_USAGE_GB\": {\"name\": \"memory\", \"unit\": \"GB\", \"decimals\": 2},\n \"MEMORY_LIMIT_GB\": {\"name\": \"memory_limit\", \"unit\": \"GB\", \"decimals\": 2},\n \"CPU_LIMIT\": {\"name\": \"cpu_limit\", \"unit\": \"vCPU\", \"decimals\": 2},\n \"DISK_USAGE_GB\": {\"name\": \"disk\", \"unit\": \"GB\", \"decimals\": 2},\n \"NETWORK_RX_GB\": {\"name\": \"network_rx\", \"unit\": \"GB\", \"decimals\": 3},\n \"NETWORK_TX_GB\": {\"name\": \"network_tx\", \"unit\": \"GB\", \"decimals\": 3},\n }\n\n # Determine the 24h cutoff timestamp\n now_ts = int(datetime.now(timezone.utc).timestamp())\n cutoff_24h = now_ts - (24 * 3600)\n\n produce_short_window = hours > 24\n\n full_window: Dict[str, Any] = {}\n short_window: Dict[str, Any] = {}\n\n for measurement, values in raw_series.items():\n info = metric_info.get(measurement)\n if not info or len(values) \u003c 2:\n continue\n\n nums = [v[\"value\"] for v in values if v.get(\"value\") is not None]\n if not nums:\n continue\n\n d = info[\"decimals\"]\n name = info[\"name\"]\n\n # Full window analysis\n full_window[name] = _analyze_window(values, nums, d, info[\"unit\"])\n\n # Short window (last 24h) analysis\n if produce_short_window:\n recent_values = [v for v in values if v.get(\"ts\", 0) >= cutoff_24h]\n recent_nums = [v[\"value\"] for v in recent_values if v.get(\"value\") is not None]\n if len(recent_nums) >= 2:\n short_window[name] = _analyze_window(recent_values, recent_nums, d, info[\"unit\"])\n\n # Build the result with named windows\n windows: Dict[str, Any] = {}\n\n # Label the full window\n if hours >= 168:\n full_label = \"7d\"\n elif hours >= 72:\n full_label = f\"{hours // 24}d\"\n else:\n full_label = f\"{hours}h\"\n\n windows[full_label] = {\n \"window_hours\": hours,\n \"metrics\": full_window,\n }\n\n if produce_short_window and short_window:\n windows[\"24h\"] = {\n \"window_hours\": 24,\n \"metrics\": short_window,\n }\n\n return {\"windows\": windows}\n\n\ndef info(msg: str) -> None:\n \"\"\"Print an [INFO] message to stdout.\"\"\"\n print(f\"[INFO] {msg}\")\n\n\ndef error(msg: str) -> None:\n \"\"\"Print an [ERROR] message to stderr and exit.\"\"\"\n print(f\"[ERROR] {msg}\", file=sys.stderr)\n sys.exit(1)\n\n\ndef confirm_with_user(prompt: str) -> bool:\n \"\"\"Get confirmation directly from the terminal.\n\n Reads from /dev/tty to ensure it's an actual user at a terminal,\n not piped input. This prevents automated scripts from bypassing confirmation.\n \"\"\"\n try:\n with open('/dev/tty', 'r') as tty:\n print(prompt, end=' ', flush=True)\n response = tty.readline().strip().lower()\n return response in ('y', 'yes')\n except (OSError, IOError):\n print(\"\\n[ERROR] This command requires interactive terminal confirmation.\")\n print(\"It cannot be run with piped input or in non-interactive mode.\")\n print(\"Please run this command directly in a terminal.\")\n return False\n\n\ndef _safe_int(val: Any, default: int = 0) -> int:\n \"\"\"Safely convert a value to int, returning default on failure.\"\"\"\n try:\n return int(val)\n except (ValueError, TypeError):\n return default\n\n\ndef _safe_float(val: Any, default: float = 0.0) -> float:\n \"\"\"Safely convert a value to float, returning default on failure.\"\"\"\n try:\n return float(val)\n except (ValueError, TypeError):\n return default\n\n\ndef _format_uptime(seconds: int) -> str:\n \"\"\"Format uptime seconds into a human-readable string.\"\"\"\n if seconds \u003c= 0:\n return \"N/A\"\n days = seconds // 86400\n hours = (seconds % 86400) // 3600\n minutes = (seconds % 3600) // 60\n parts = []\n if days > 0:\n parts.append(f\"{days}d\")\n if hours > 0:\n parts.append(f\"{hours}h\")\n if minutes > 0 and days == 0:\n parts.append(f\"{minutes}m\")\n return \" \".join(parts) if parts else \"\u003c 1m\"\n\n\ndef _trend_indicator(metrics_history: Optional[Dict[str, Any]], metric_name: str) -> str:\n \"\"\"Return a compact trend string like ' (^ +15.2% 24h)' for use in summary rows.\"\"\"\n if not metrics_history:\n return \"\"\n windows = metrics_history.get(\"windows\", {})\n window_data = windows.get(\"24h\") or next(iter(windows.values()), None) if windows else None\n if not window_data or not window_data.get(\"metrics\"):\n return \"\"\n m = window_data[\"metrics\"].get(metric_name)\n if not m or \"trend\" not in m:\n return \"\"\n t = m[\"trend\"]\n direction = t.get(\"direction\", \"stable\")\n change = t.get(\"change_pct\", 0)\n window_label = \"24h\" if \"24h\" in windows else next(iter(windows), \"\")\n arrow = {\"increasing\": \"^\", \"decreasing\": \"v\", \"stable\": \"~\"}.get(direction, \"\")\n if direction == \"stable\":\n return f\" ({arrow} stable {window_label})\"\n return f\" ({arrow} {change:+.1f}% {window_label})\"\n\n\ndef get_recent_logs(service: str, lines: int = LOG_LINES_DEFAULT,\n environment_id: Optional[str] = None,\n service_id: Optional[str] = None) -> List[str]:\n \"\"\"Get recent logs for LLM analysis.\n\n Uses API if environment_id and service_id provided (~3s),\n retries once with longer timeout on failure,\n falls back to CLI (~27s for 100 lines).\n \"\"\"\n # Fast path: use API directly\n if environment_id and service_id:\n script_dir = os.path.dirname(os.path.abspath(__file__))\n api_script = os.path.join(script_dir, \"railway-api.sh\")\n\n if os.path.exists(api_script):\n # Use environmentLogs API with service filter\n query = f'''query {{\n environmentLogs(\n environmentId: \"{environment_id}\",\n beforeLimit: {lines},\n filter: \"@service:{service_id}\"\n ) {{\n timestamp\n message\n }}\n }}'''\n # Try API twice: first with 15s timeout, retry with 30s\n for attempt_timeout in [15, 30]:\n try:\n result = subprocess.run(\n [api_script, query],\n capture_output=True,\n text=True,\n timeout=attempt_timeout\n )\n if result.returncode == 0:\n data = json.loads(result.stdout)\n logs_data = data.get(\"data\", {}).get(\"environmentLogs\", [])\n if logs_data:\n return [f\"{log['timestamp']} {log['message']}\" for log in logs_data]\n except (subprocess.TimeoutExpired, json.JSONDecodeError):\n pass\n\n # Fallback: use CLI (slow, ~27s)\n code, stdout, stderr = run_railway_command(\n [\"logs\"] + _ctx.logs_flags() + [\"--service\", service, \"--lines\", str(lines)],\n timeout=30\n )\n if code != 0:\n return []\n\n logs = []\n for line in stdout.strip().split(\"\\n\"):\n if line.strip():\n logs.append(line.strip())\n return logs\n","content_type":"text/x-python; charset=utf-8","language":"python","size":23510,"content_sha256":"fe74d383242446c04c8b60bd3ca89441f2d9fe56ee5ceb8493d5c0b5cc30e640"},{"filename":"scripts/enable-pg-stats.py","content":"#!/usr/bin/env python3\n\"\"\"\nEnable pg_stat_statements for query statistics tracking.\n\nThis script replicates the frontend's Metrics.tsx enable logic:\n1. Check if pg_stat_statements is already in shared_preload_libraries\n2. If yes, just install the extension\n3. If no, install extension + ALTER SYSTEM + restart\n\nUsage:\n enable-pg-stats.py --service \u003cname>\n\nRequires: railway CLI linked to the correct project/environment\n\nIMPORTANT: This script requires interactive terminal confirmation.\nIt cannot be run with piped input - the user must confirm directly.\n\"\"\"\n\nimport argparse\nimport sys\nimport re\nfrom typing import List\n\nfrom dal import run_psql_query, info, error, confirm_with_user\n\n\ndef parse_preload_libraries(value: str) -> List[str]:\n \"\"\"Parse shared_preload_libraries value into clean library names.\"\"\"\n if not value or not value.strip():\n return []\n\n # Split by comma and clean up quotes\n libs = []\n for lib in value.split(\",\"):\n clean = lib.strip().replace('\"', '').replace(\"'\", '')\n # Validate as PostgreSQL identifier\n if clean and re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*

Use Railway Railway resource model Railway organizes infrastructure in a hierarchy: - Workspace is the billing and team scope. A user belongs to one or more workspaces. - Project is a collection of services under one workspace. It maps to one deployable unit of work. - Environment is an isolated configuration plane inside a project (for example, , ). Each environment has its own variables, config, and deployment history. - Service is a single deployable unit inside a project. It can be an app from a repo, a Docker image, or a managed database. - Bucket is an S3-compatible object storage resou…

, clean):\n libs.append(clean)\n return libs\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Enable pg_stat_statements for query performance tracking.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nWhat this does:\n 1. Checks if pg_stat_statements is already in shared_preload_libraries\n 2. Installs the extension: CREATE EXTENSION IF NOT EXISTS pg_stat_statements\n 3. If library wasn't preloaded: configures shared_preload_libraries and restarts\n\nNote: If restart is needed, the database will have brief downtime.\n\nIMPORTANT: This script requires interactive terminal confirmation and cannot\nbe automated. The user must confirm the action directly.\n \"\"\"\n )\n\n parser.add_argument(\"--service\", required=True, help=\"Service name (requires linked project)\")\n\n args = parser.parse_args()\n service = args.service\n\n # Step 1: Check current shared_preload_libraries\n info(\"Checking current shared_preload_libraries...\")\n code, output = run_psql_query(service, \"SHOW shared_preload_libraries\")\n if code != 0:\n error(f\"Failed to query shared_preload_libraries: {output}\")\n\n current_libs = output\n info(f\"Current shared_preload_libraries: {current_libs or '\u003cempty>'}\")\n\n # Check if pg_stat_statements is already loaded\n existing_libs = parse_preload_libraries(current_libs)\n library_already_loaded = \"pg_stat_statements\" in existing_libs\n\n if library_already_loaded:\n info(\"pg_stat_statements is already in shared_preload_libraries\")\n needs_restart = False\n else:\n info(\"pg_stat_statements is NOT in shared_preload_libraries (will need restart)\")\n needs_restart = True\n\n # Step 2: Check if extension is already installed\n info(\"Checking if pg_stat_statements extension is installed...\")\n code, output = run_psql_query(service, \"SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements'\")\n\n extension_exists = code == 0 and output.strip() == \"1\"\n\n if extension_exists:\n info(\"pg_stat_statements extension is already installed\")\n needs_install = False\n else:\n info(\"pg_stat_statements extension needs to be installed\")\n needs_install = True\n\n # If everything is already configured, exit early\n if not needs_restart and not needs_install:\n info(\"pg_stat_statements is fully configured and ready!\")\n print(\"\")\n print(\"Query statistics are available. Run:\")\n print(\" SELECT * FROM pg_stat_statements LIMIT 10;\")\n return 0\n\n # Confirm before making changes - REQUIRES interactive terminal\n print(\"\")\n print(f\"About to make the following changes to '{service}':\")\n if needs_install:\n print(\" - Install pg_stat_statements extension\")\n if needs_restart:\n print(\" - Configure shared_preload_libraries\")\n print(\" - Restart the database (brief downtime)\")\n print(\"\")\n\n if not confirm_with_user(\"Continue? [y/N]\"):\n print(\"Cancelled.\")\n return 1\n\n # Step 3: Install extension\n if needs_install:\n info(\"Installing pg_stat_statements extension...\")\n code, output = run_psql_query(service, \"CREATE EXTENSION IF NOT EXISTS pg_stat_statements\")\n if code != 0:\n error(f\"Failed to install extension: {output}\")\n info(\"Extension installed\")\n\n # Step 4: Configure shared_preload_libraries and restart if needed\n if needs_restart:\n info(\"Configuring shared_preload_libraries...\")\n\n # Add pg_stat_statements to existing libraries\n if \"pg_stat_statements\" not in existing_libs:\n existing_libs.append(\"pg_stat_statements\")\n\n # Build ALTER SYSTEM with individual quoted values\n # PostgreSQL syntax: ALTER SYSTEM SET shared_preload_libraries TO 'lib1', 'lib2'\n quoted_libs = \", \".join(f\"'{lib}'\" for lib in existing_libs)\n alter_query = f\"ALTER SYSTEM SET shared_preload_libraries TO {quoted_libs}\"\n\n info(f\"Setting shared_preload_libraries to: {', '.join(existing_libs)}\")\n\n code, output = run_psql_query(service, alter_query)\n if code != 0:\n error(f\"Failed to configure shared_preload_libraries: {output}\")\n info(\"shared_preload_libraries configured\")\n\n # Step 5: Restart the service\n info(\"Restarting database service...\")\n code, stdout, stderr = run_railway_command([\"restart\", \"--service\", service, \"--yes\"])\n if code != 0:\n error(f\"Failed to restart service: {stderr or stdout}\")\n\n print(\"\")\n info(\"Database is restarting. Query statistics will be available shortly.\")\n print(\"\")\n print(\"After restart completes, verify with:\")\n print(\" SHOW shared_preload_libraries;\")\n print(\" SELECT * FROM pg_stat_statements LIMIT 5;\")\n else:\n print(\"\")\n info(\"pg_stat_statements is now ready!\")\n print(\"\")\n print(\"Query statistics are available. Run:\")\n print(\" SELECT * FROM pg_stat_statements LIMIT 10;\")\n\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6241,"content_sha256":"eea271c4eb7e794920cfee2a5ba98c892ae75b9d126522d9b79f315d1e7058cd"},{"filename":"scripts/pg-extensions.py","content":"#!/usr/bin/env python3\n\"\"\"\nManage PostgreSQL extensions for Railway database services.\n\nThis script replicates the frontend's Extensions.tsx logic:\n- List available and installed extensions\n- Install extensions with automatic dependency handling\n- Uninstall extensions with dependency checking\n\nUsage:\n pg-extensions.py --service \u003cname> list\n pg-extensions.py --service \u003cname> install \u003cextension> [--version \u003cver>]\n pg-extensions.py --service \u003cname> uninstall \u003cextension>\n pg-extensions.py --service \u003cname> info \u003cextension>\n\nRequires: railway CLI linked to the correct project/environment\n\nIMPORTANT: Install and uninstall require interactive terminal confirmation.\nThey cannot be run with piped input - the user must confirm directly.\n\"\"\"\n\nimport argparse\nimport sys\nimport json\nfrom typing import Tuple, List, Optional, Dict, Any\nfrom dataclasses import dataclass\n\nfrom dal import run_psql_query, info, error, confirm_with_user\n\n\n@dataclass\nclass Extension:\n \"\"\"PostgreSQL extension info.\"\"\"\n name: str\n default_version: str\n installed_version: Optional[str]\n comment: str\n\n\ndef list_extensions(service: str, json_output: bool = False) -> List[Extension]:\n \"\"\"List all available and installed extensions.\"\"\"\n # Query available extensions\n available_query = \"SELECT name, default_version, comment FROM pg_available_extensions ORDER BY name\"\n code, output = run_psql_query(service, available_query)\n if code != 0:\n error(f\"Failed to query available extensions: {output}\")\n\n # Parse available extensions\n available = {}\n for line in output.strip().split(\"\\n\"):\n if not line:\n continue\n parts = line.split(\"|\")\n if len(parts) >= 2:\n name = parts[0].strip()\n version = parts[1].strip()\n comment = parts[2].strip() if len(parts) > 2 else \"\"\n available[name] = {\"version\": version, \"comment\": comment}\n\n # Query installed extensions\n installed_query = \"SELECT extname, extversion FROM pg_extension\"\n code, output = run_psql_query(service, installed_query)\n if code != 0:\n error(f\"Failed to query installed extensions: {output}\")\n\n # Parse installed extensions\n installed = {}\n for line in output.strip().split(\"\\n\"):\n if not line:\n continue\n parts = line.split(\"|\")\n if len(parts) >= 2:\n name = parts[0].strip()\n version = parts[1].strip()\n installed[name] = version\n\n # Build extension list\n extensions = []\n for name, info in available.items():\n ext = Extension(\n name=name,\n default_version=info[\"version\"],\n installed_version=installed.get(name),\n comment=info[\"comment\"]\n )\n extensions.append(ext)\n\n if json_output:\n print(json.dumps([{\n \"name\": e.name,\n \"defaultVersion\": e.default_version,\n \"installedVersion\": e.installed_version,\n \"comment\": e.comment\n } for e in extensions], indent=2))\n else:\n # Print formatted output\n installed_exts = [e for e in extensions if e.installed_version]\n available_exts = [e for e in extensions if not e.installed_version]\n\n print(f\"\\n{len(installed_exts)} Extension(s) installed:\")\n print(\"-\" * 60)\n if installed_exts:\n for e in sorted(installed_exts, key=lambda x: x.name):\n print(f\" {e.name} (v{e.installed_version})\")\n else:\n print(\" (none)\")\n\n print(f\"\\n{len(available_exts)} Extension(s) available:\")\n print(\"-\" * 60)\n for e in sorted(available_exts, key=lambda x: x.name)[:30]:\n desc = f\" - {e.comment[:50]}...\" if e.comment and len(e.comment) > 50 else f\" - {e.comment}\" if e.comment else \"\"\n print(f\" {e.name} (v{e.default_version}){desc}\")\n if len(available_exts) > 30:\n print(f\" ... and {len(available_exts) - 30} more\")\n\n return extensions\n\n\ndef get_extension_dependencies(service: str, extension: str) -> List[str]:\n \"\"\"Get dependencies for an extension.\"\"\"\n query = f\"\"\"\n SELECT DISTINCT unnest(pev.requires) as dependency\n FROM pg_available_extension_versions pev\n WHERE pev.name = '{extension}' AND pev.requires IS NOT NULL\n ORDER BY dependency\n \"\"\"\n code, output = run_psql_query(service, query)\n if code != 0:\n return []\n\n deps = []\n for line in output.strip().split(\"\\n\"):\n if line.strip():\n deps.append(line.strip())\n return deps\n\n\ndef get_extension_dependents(service: str, extension: str) -> List[str]:\n \"\"\"Get extensions that depend on this extension.\"\"\"\n query = f\"\"\"\n SELECT e.extname AS dependent_extension\n FROM pg_depend d\n JOIN pg_extension e ON d.objid = e.oid\n JOIN pg_extension ref_e ON d.refobjid = ref_e.oid\n WHERE d.classid = 'pg_extension'::regclass\n AND d.refclassid = 'pg_extension'::regclass\n AND ref_e.extname = '{extension}'\n ORDER BY dependent_extension\n \"\"\"\n code, output = run_psql_query(service, query)\n if code != 0:\n return []\n\n dependents = []\n for line in output.strip().split(\"\\n\"):\n if line.strip():\n dependents.append(line.strip())\n return dependents\n\n\ndef is_extension_installed(service: str, extension: str) -> Tuple[bool, Optional[str]]:\n \"\"\"Check if extension is installed, return (installed, version).\"\"\"\n query = f\"SELECT extversion FROM pg_extension WHERE extname = '{extension}'\"\n code, output = run_psql_query(service, query)\n if code == 0 and output.strip():\n return True, output.strip()\n return False, None\n\n\ndef is_extension_available(service: str, extension: str) -> bool:\n \"\"\"Check if extension is available in the image.\"\"\"\n query = f\"SELECT 1 FROM pg_available_extensions WHERE name = '{extension}'\"\n code, output = run_psql_query(service, query)\n return code == 0 and output.strip() == \"1\"\n\n\ndef install_extension(service: str, extension: str, version: Optional[str] = None) -> bool:\n \"\"\"Install an extension with CASCADE (auto-install dependencies).\"\"\"\n # Check if available\n if not is_extension_available(service, extension):\n error(f\"Extension '{extension}' is not available in this database image\")\n\n # Check if already installed\n installed, curr_ver = is_extension_installed(service, extension)\n if installed:\n info(f\"Extension '{extension}' is already installed (v{curr_ver})\")\n return True\n\n # Check dependencies and confirm - REQUIRES interactive terminal\n deps = get_extension_dependencies(service, extension)\n if deps:\n print(f\"\\nInstalling '{extension}' will also install these dependencies: {', '.join(deps)}\")\n else:\n print(f\"\\nAbout to install extension '{extension}'\")\n\n if not confirm_with_user(\"Continue? [y/N]\"):\n print(\"Cancelled.\")\n return False\n\n # Build install query\n query = f'CREATE EXTENSION IF NOT EXISTS \"{extension}\"'\n if version:\n query += f\" VERSION '{version}'\"\n query += \" CASCADE\" # Auto-install dependencies\n\n info(f\"Installing extension '{extension}'...\")\n code, output = run_psql_query(service, query)\n if code != 0:\n error(f\"Failed to install extension: {output}\")\n\n # Verify installation\n installed, ver = is_extension_installed(service, extension)\n if installed:\n info(f\"Successfully installed '{extension}' (v{ver})\")\n if deps:\n info(f\"Dependencies installed: {', '.join(deps)}\")\n return True\n else:\n error(\"Extension installation failed - not found after CREATE EXTENSION\")\n return False\n\n\ndef uninstall_extension(service: str, extension: str) -> bool:\n \"\"\"Uninstall an extension.\"\"\"\n # Check if installed\n installed, ver = is_extension_installed(service, extension)\n if not installed:\n info(f\"Extension '{extension}' is not installed\")\n return True\n\n # Check for dependents\n dependents = get_extension_dependents(service, extension)\n if dependents:\n error(f\"Cannot uninstall '{extension}' - these extensions depend on it: {', '.join(dependents)}\")\n\n # Confirm - REQUIRES interactive terminal\n print(f\"\\nAbout to uninstall '{extension}' (v{ver})\")\n if not confirm_with_user(\"Continue? [y/N]\"):\n print(\"Cancelled.\")\n return False\n\n # Uninstall\n query = f'DROP EXTENSION IF EXISTS \"{extension}\"'\n info(f\"Uninstalling extension '{extension}'...\")\n code, output = run_psql_query(service, query)\n if code != 0:\n error(f\"Failed to uninstall extension: {output}\")\n\n # Verify\n installed, _ = is_extension_installed(service, extension)\n if not installed:\n info(f\"Successfully uninstalled '{extension}'\")\n return True\n else:\n error(\"Extension uninstall failed - still found after DROP EXTENSION\")\n return False\n\n\ndef extension_info(service: str, extension: str, json_output: bool = False):\n \"\"\"Show detailed info about an extension.\"\"\"\n # Check availability\n available = is_extension_available(service, extension)\n if not available:\n error(f\"Extension '{extension}' is not available in this database image\")\n\n # Get info\n query = f\"SELECT name, default_version, comment FROM pg_available_extensions WHERE name = '{extension}'\"\n code, output = run_psql_query(service, query)\n if code != 0:\n error(f\"Failed to get extension info: {output}\")\n\n parts = output.strip().split(\"|\")\n name = parts[0].strip() if parts else extension\n default_version = parts[1].strip() if len(parts) > 1 else \"unknown\"\n comment = parts[2].strip() if len(parts) > 2 else \"\"\n\n installed, installed_version = is_extension_installed(service, extension)\n deps = get_extension_dependencies(service, extension)\n dependents = get_extension_dependents(service, extension) if installed else []\n\n if json_output:\n print(json.dumps({\n \"name\": name,\n \"defaultVersion\": default_version,\n \"installedVersion\": installed_version,\n \"comment\": comment,\n \"dependencies\": deps,\n \"dependents\": dependents\n }, indent=2))\n else:\n print(f\"\\nExtension: {name}\")\n print(\"-\" * 40)\n print(f\" Default Version: {default_version}\")\n print(f\" Installed: {'Yes (v' + installed_version + ')' if installed else 'No'}\")\n if comment:\n print(f\" Description: {comment}\")\n if deps:\n print(f\" Dependencies: {', '.join(deps)}\")\n if dependents:\n print(f\" Required by: {', '.join(dependents)}\")\n print()\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Manage PostgreSQL extensions for Railway services.\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\nExamples:\n List all extensions (safe, no confirmation needed):\n pg-extensions.py --service my-postgres list\n\n Install an extension (requires interactive confirmation):\n pg-extensions.py --service my-postgres install postgis\n pg-extensions.py --service my-postgres install pgvector --version 0.5.0\n\n Uninstall an extension (requires interactive confirmation):\n pg-extensions.py --service my-postgres uninstall postgis\n\n Get extension info (safe, no confirmation needed):\n pg-extensions.py --service my-postgres info pg_stat_statements\n\nIMPORTANT: Install and uninstall require interactive terminal confirmation.\nThey cannot be automated or run with piped input.\n \"\"\"\n )\n\n parser.add_argument(\"--service\", required=True, help=\"Service name (requires linked project)\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"Output as JSON\")\n\n subparsers = parser.add_subparsers(dest=\"command\", help=\"Command to run\")\n\n # list command (safe - no confirmation)\n list_parser = subparsers.add_parser(\"list\", help=\"List available and installed extensions\")\n\n # install command (requires interactive confirmation)\n install_parser = subparsers.add_parser(\"install\", help=\"Install an extension (requires confirmation)\")\n install_parser.add_argument(\"extension\", help=\"Extension name\")\n install_parser.add_argument(\"--version\", \"-v\", help=\"Specific version to install\")\n\n # uninstall command (requires interactive confirmation)\n uninstall_parser = subparsers.add_parser(\"uninstall\", help=\"Uninstall an extension (requires confirmation)\")\n uninstall_parser.add_argument(\"extension\", help=\"Extension name\")\n\n # info command\n info_parser = subparsers.add_parser(\"info\", help=\"Show extension info\")\n info_parser.add_argument(\"extension\", help=\"Extension name\")\n\n args = parser.parse_args()\n\n if not args.command:\n parser.print_help()\n return 1\n\n service = args.service\n\n if args.command == \"list\":\n list_extensions(service, json_output=args.json)\n elif args.command == \"install\":\n install_extension(service, args.extension, version=args.version)\n elif args.command == \"uninstall\":\n uninstall_extension(service, args.extension)\n elif args.command == \"info\":\n extension_info(service, args.extension, json_output=args.json)\n\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13363,"content_sha256":"443b34c6fe7418090e426a8830062106f4f89309a754c684d7b9e720e443fc9d"},{"filename":"scripts/railway-api.sh","content":"#!/usr/bin/env bash\n# Railway GraphQL API helper\n# Usage: railway-api.sh '\u003cgraphql-query>' ['\u003cvariables-json>']\n\nset -e\n\nSKILL_ID=\"use-railway\"\nSKILL_VERSION=\"${RAILWAY_SKILL_VERSION:-1.2.3}\"\n\nexport RAILWAY_CALLER=\"${RAILWAY_CALLER:-skill:${SKILL_ID}@${SKILL_VERSION}}\"\nexport RAILWAY_AGENT_SESSION=\"${RAILWAY_AGENT_SESSION:-railway-skill-$(date +%s)-$}\"\n\nif ! command -v jq &>/dev/null; then\n echo '{\"error\": \"jq not installed. Install with: brew install jq\"}'\n exit 1\nfi\n\nCONFIG_FILE=\"$HOME/.railway/config.json\"\n\nif [[ ! -f \"$CONFIG_FILE\" ]]; then\n echo '{\"error\": \"Railway config not found. Run: railway login\"}'\n exit 1\nfi\n\nTOKEN=$(jq -r '.user.token' \"$CONFIG_FILE\")\n\nif [[ -z \"$TOKEN\" || \"$TOKEN\" == \"null\" ]]; then\n echo '{\"error\": \"No Railway token found. Run: railway login\"}'\n exit 1\nfi\n\nif [[ -z \"$1\" ]]; then\n echo '{\"error\": \"No query provided\"}'\n exit 1\nfi\n\n# Build payload with query and optional variables\nif [[ -n \"$2\" ]]; then\n PAYLOAD=$(jq -n --arg q \"$1\" --argjson v \"$2\" '{query: $q, variables: $v}')\nelse\n PAYLOAD=$(jq -n --arg q \"$1\" '{query: $q}')\nfi\n\nHEADERS=(\n -H \"Content-Type: application/json\"\n -H \"X-Railway-Skill-Id: $SKILL_ID\"\n -H \"X-Railway-Skill-Version: $SKILL_VERSION\"\n -H \"X-Railway-Agent-Session: $RAILWAY_AGENT_SESSION\"\n)\n\n# Keep the token and the request body out of argv: /proc/\u003cpid>/cmdline is\n# readable by any other local user, so anything passed as an argument leaks on a\n# shared host. The bearer goes in through --config on a pipe, the payload on stdin.\nprintf '%s' \"$PAYLOAD\" | curl -s https://backboard.railway.com/graphql/v2 \\\n \"${HEADERS[@]}\" \\\n --config \u003c(printf 'header = \"Authorization: Bearer %s\"\\n' \"$TOKEN\") \\\n -d @-\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1694,"content_sha256":"45e9f397ed733f6f3a0e55476d0824eff86b30371be2699c55ffbefb3b28b9b0"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Use Railway","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Railway resource model","type":"text"}]},{"type":"paragraph","content":[{"text":"Railway organizes infrastructure in a hierarchy:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Workspace","type":"text","marks":[{"type":"strong"}]},{"text":" is the billing and team scope. A user belongs to one or more workspaces.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Project","type":"text","marks":[{"type":"strong"}]},{"text":" is a collection of services under one workspace. It maps to one deployable unit of work.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Environment","type":"text","marks":[{"type":"strong"}]},{"text":" is an isolated configuration plane inside a project (for example, ","type":"text"},{"text":"production","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"staging","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Each environment has its own variables, config, and deployment history.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Service","type":"text","marks":[{"type":"strong"}]},{"text":" is a single deployable unit inside a project. It can be an app from a repo, a Docker image, or a managed database.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bucket","type":"text","marks":[{"type":"strong"}]},{"text":" is an S3-compatible object storage resource inside a project. Buckets are created at the project level and deployed to environments. Each bucket has credentials (endpoint, access key, secret key) for S3-compatible access.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deployment","type":"text","marks":[{"type":"strong"}]},{"text":" is a point-in-time release of a service in an environment. It has build logs, runtime logs, and a status lifecycle.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Most CLI commands operate on the linked project/environment/service context. Use ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to see the context, and ","type":"text"},{"text":"--project","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--environment","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--service","type":"text","marks":[{"type":"code_inline"}]},{"text":" flags to override.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Tool routing","type":"text"}]},{"type":"paragraph","content":[{"text":"Railway has three agent-facing operation paths. Choose the path that matches the job:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Railway CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"railway","type":"text","marks":[{"type":"code_inline"}]},{"text":"): workflows that depend on local machine state such as current working directory deploys, ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"railway run","type":"text","marks":[{"type":"code_inline"}]},{"text":", SSH, database analysis scripts, local linking, interactive setup, or exact command output.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Remote MCP","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"https://mcp.railway.com","type":"text","marks":[{"type":"code_inline"}]},{"text":"): default plugin MCP path for account/project/service discovery, deployment state, bounded logs, traces, feature flags, simple redeploys, simple project creation, or complex Railway workflows that can be handed to ","type":"text"},{"text":"railway-agent","type":"text","marks":[{"type":"code_inline"}]},{"text":". Remote MCP uses Railway OAuth and does not depend on local CLI state.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GraphQL through ","type":"text","marks":[{"type":"strong"}]},{"text":"railway api","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":": operations without a dedicated MCP tool or CLI command. Use schema search and inspection before constructing unfamiliar queries.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If multiple paths are available, choose the one that preserves the needed context. The CLI fits workflows that need the current repo, local credentials, SSH, database scripts, or exact command output. Remote MCP fits OAuth-scoped platform operations that do not need local files or CLI state.","type":"text"}]},{"type":"paragraph","content":[{"text":"On a Railway cloud agent VM the ","type":"text"},{"text":"railway","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI only has credentials inside an SSH terminal session. In a dashboard or mobile chat session (Railway Agent) it is unauthenticated by design: do not run ","type":"text"},{"text":"railway","type":"text","marks":[{"type":"code_inline"}]},{"text":" commands there, not even reads or ","type":"text"},{"text":"railway api","type":"text","marks":[{"type":"code_inline"}]},{"text":". The ","type":"text"},{"text":"railway","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP server is authenticated in every session, so use its tools, resolve IDs with ","type":"text"},{"text":"list-services","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead of ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":", and when no tool covers the job say so and ask the user to make the change in the dashboard.","type":"text"}]},{"type":"paragraph","content":[{"text":"Optional: an already configured in-process CLI MCP (","type":"text"},{"text":"railway mcp local","type":"text","marks":[{"type":"code_inline"}]},{"text":") can supply operations not available through hosted MCP. A bare ","type":"text"},{"text":"railway mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" now starts the hosted MCP proxy using CLI authentication; it is not the in-process server. Published plugin configs connect directly to hosted MCP with editor OAuth.","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer ","type":"text"},{"text":"railway api","type":"text","marks":[{"type":"code_inline"}]},{"text":" (CLI 5.28+) for GraphQL execution. The legacy ","type":"text"},{"text":"scripts/railway-api.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" remains a compatibility fallback for older CLIs; see ","type":"text"},{"text":"request.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/request.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Parsing Railway URLs","type":"text"}]},{"type":"paragraph","content":[{"text":"Users often paste Railway dashboard URLs. Extract IDs before doing anything else:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"https://railway.com/project/\u003cPROJECT_ID>/service/\u003cSERVICE_ID>?environmentId=\u003cENV_ID>\nhttps://railway.com/project/\u003cPROJECT_ID>/service/\u003cSERVICE_ID>","type":"text"}]},{"type":"paragraph","content":[{"text":"The URL always contains ","type":"text"},{"text":"projectId","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"serviceId","type":"text","marks":[{"type":"code_inline"}]},{"text":". It may contain ","type":"text"},{"text":"environmentId","type":"text","marks":[{"type":"code_inline"}]},{"text":" as a query parameter. If the environment ID is missing and the user specifies an environment by name (e.g., \"production\"), resolve it:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway api \\\n 'query getProject($id: String!) {\n project(id: $id) {\n environments { edges { node { id name } } }\n }\n }' \\\n --variables '{\"id\": \"\u003cPROJECT_ID>\"}'","type":"text"}]},{"type":"paragraph","content":[{"text":"Match the environment name (case-insensitive) to get the ","type":"text"},{"text":"environmentId","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer passing explicit IDs","type":"text","marks":[{"type":"strong"}]},{"text":" to CLI commands (","type":"text"},{"text":"--project","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--environment","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--service","type":"text","marks":[{"type":"code_inline"}]},{"text":") and scripts (","type":"text"},{"text":"--project-id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--environment-id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--service-id","type":"text","marks":[{"type":"code_inline"}]},{"text":") instead of running ","type":"text"},{"text":"railway link","type":"text","marks":[{"type":"code_inline"}]},{"text":". This avoids modifying global state and is faster.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Intent-based routing","type":"text"}]},{"type":"paragraph","content":[{"text":"Route by user intent ","type":"text"},{"text":"before","type":"text","marks":[{"type":"em"}]},{"text":" running preflight checks. The preflight ceremony below is for diagnostic and configuration work — it adds friction when the user just wants to ship something or sign up.","type":"text"}]},{"type":"paragraph","content":[{"text":"Deploy-from-cwd intent","type":"text","marks":[{"type":"strong"}]},{"text":" (\"deploy\", \"ship\", \"push to Railway\", \"deploy this app\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skip the ","type":"text"},{"text":"railway whoami","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"railway status","type":"text","marks":[{"type":"code_inline"}]},{"text":" preflights.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" directly — it self-validates auth, signs the user in (the CLI opens a browser) if they're unauthenticated, and chains into project + service creation and deploy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Announce intent before invoking: ","type":"text"},{"text":"\"Running ","type":"text","marks":[{"type":"em"}]},{"text":"railway up","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":" — it'll sign you in if needed and deploy this directory.\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT ask the user to run ","type":"text","marks":[{"type":"strong"}]},{"text":"railway login","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" first.","type":"text","marks":[{"type":"strong"}]},{"text":" The chain handles auth as part of the deploy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the environment can't open a browser, the CLI prints a device-code sign-in link and waits — follow ","type":"text"},{"text":"Device-code sign-in: relay the link immediately","type":"text","marks":[{"type":"link","attrs":{"href":"#account-creation--sign-in","title":null}}]},{"text":" (run in background, relay the link to the user the moment it prints).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Signup intent","type":"text","marks":[{"type":"strong"}]},{"text":" (\"sign me up\", \"create my Railway account\", \"register me\", \"get me on Railway\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the current directory has a deployable app (e.g. ","type":"text","marks":[{"type":"strong"}]},{"text":"package.json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", ","type":"text","marks":[{"type":"strong"}]},{"text":"requirements.txt","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", ","type":"text","marks":[{"type":"strong"}]},{"text":"go.mod","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", ","type":"text","marks":[{"type":"strong"}]},{"text":"Dockerfile","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":", source to build), run ","type":"text","marks":[{"type":"strong"}]},{"text":"railway up","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" — it signs the user up ","type":"text"},{"text":"and","type":"text","marks":[{"type":"em"}]},{"text":" deploys in one shot, landing them on a running app. A detected agent harness authorizes the project creation, so ","type":"text"},{"text":"bare ","type":"text","marks":[{"type":"strong"}]},{"text":"railway up","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" is enough","type":"text","marks":[{"type":"strong"}]},{"text":" — there's no extra prompt to clear. Use it even when the user only said \"sign me up\": shipping their app is the goal, so don't make them pick a command and don't drop to a bare login. For scripted or agent runs, ","type":"text"},{"text":"railway up -y","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the robust form — it skips prompts and forces the create non-interactively even if harness detection misses. ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" is NOT the default for signup when there's something to deploy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Only when there is nothing to deploy","type":"text","marks":[{"type":"strong"}]},{"text":" — an empty / non-app directory, or the user explicitly says they just want an account with no deploy — use ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" (creates new accounts on the fly through the same OAuth surface). There is no separate signup command.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Signup is the flow most likely to hit the device-code wait (brand-new users in sandboxed/headless agent environments). Follow ","type":"text"},{"text":"Device-code sign-in: relay the link immediately","type":"text","marks":[{"type":"link","attrs":{"href":"#account-creation--sign-in","title":null}}]},{"text":" — a signup lost to an expired code is a lost user, not a retry.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Sandbox / remote-build intent","type":"text","marks":[{"type":"strong"}]},{"text":" (\"give me a sandbox\", \"spin up a scratch environment\", \"build this remotely\", \"run this remotely\", \"checkpoint/snapshot the sandbox\", \"save this sandbox state\", \"restore my sandbox\"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load ","type":"text"},{"text":"sandbox.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sandbox.md","title":null}}]},{"text":" and follow it. Sandboxes require the feature to be enabled in Priority Boarding — if a sandbox command fails with a feature-availability error, prompt the user to enable Sandboxes in Priority Boarding rather than retrying.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Other intents","type":"text","marks":[{"type":"strong"}]},{"text":" (querying state, listing projects, configuring variables, debugging failures):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow the Preflight section below.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Preflight","type":"text"}]},{"type":"paragraph","content":[{"text":"Before any mutation, verify the tool path and context:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"command -v railway # CLI installed\nRAILWAY_CALLER=\"skill:[email protected]\" RAILWAY_AGENT_SESSION=\"railway-skill-$(date +%s)-$\" railway whoami --json\nrailway --version # check CLI version","type":"text"}]},{"type":"paragraph","content":[{"text":"Exception","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" self-validate auth and run their own unauth-aware flows. Don't run ","type":"text"},{"text":"railway whoami","type":"text","marks":[{"type":"code_inline"}]},{"text":" before them — it adds a redundant failing call without changing what you do next. See ","type":"text"},{"text":"Account creation & sign-in","type":"text","marks":[{"type":"link","attrs":{"href":"#account-creation--sign-in","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Skill freshness","type":"text"}]},{"type":"paragraph","content":[{"text":"Once per session, alongside the preflight checks, verify the installed Railway agent tooling is current. Root help ends with an ","type":"text"},{"text":"Agent tooling:","type":"text","marks":[{"type":"code_inline"}]},{"text":" health section (printed to stderr, CLI 5.9+; older CLIs print nothing — skip this check rather than retrying):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway --help 2>&1 | grep -A4 \"Agent tooling:\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Act on what it reports:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skills installed and up to date — continue; nothing to do.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"An update is available (","type":"text"},{"text":"rev \u003ca> → \u003cb> available","type":"text","marks":[{"type":"code_inline"}]},{"text":") — run ","type":"text"},{"text":"railway skills update","type":"text","marks":[{"type":"code_inline"}]},{"text":", then tell the user to restart their coding tool so the updated skills load. This session keeps running on the old skill revision until restart: finish the current request, and if this skill's guidance disagrees with the updated CLI's own output, trust the CLI.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skills or MCP server missing (","type":"text"},{"text":"✗","type":"text","marks":[{"type":"code_inline"}]},{"text":" lines) — run ","type":"text"},{"text":"railway setup agent -y","type":"text","marks":[{"type":"code_inline"}]},{"text":", then tell the user to restart their coding tool.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Check once per session and don't re-run it after acting; the restart prompt to the user is the resolution, not another check.","type":"text"}]},{"type":"paragraph","content":[{"text":"When Railway MCP is available and the job is a platform-state read, use the matching MCP read instead of shelling out. If using the CLI path, run the CLI checks above.","type":"text"}]},{"type":"paragraph","content":[{"text":"For Railway CLI calls made while this skill is active, prefix the command with ","type":"text"},{"text":"RAILWAY_CALLER=skill:[email protected]","type":"text","marks":[{"type":"code_inline"}]},{"text":" and a stable ","type":"text"},{"text":"RAILWAY_AGENT_SESSION","type":"text","marks":[{"type":"code_inline"}]},{"text":" reused for the current user request. Generate the session id once per user request, then reuse that exact value for later Railway CLI calls in the same workflow. Do not run a separate ","type":"text"},{"text":"export","type":"text","marks":[{"type":"code_inline"}]},{"text":" preflight solely for telemetry; inline env prefixes keep the shell output concise and avoid leaking setup steps into every response.","type":"text"}]},{"type":"paragraph","content":[{"text":"Context resolution - URL IDs always win:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the user provides a Railway URL, extract IDs from it. Do NOT run ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":"; it returns the locally linked project, which is usually unrelated.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If no URL is given, fall back to ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the linked project/environment/service.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When using MCP tools after resolving local context with ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":", pass the resolved project, environment, and service IDs explicitly. Do not rely on MCP implicit linked context; MCP may not share the CLI's current working directory link.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the CLI is missing, guide the user to install it.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -fsSL agents.railway.com | sh # Install CLI and configure detected agents\nbash \u003c(curl -fsSL https://railway.com/install.sh) --agents -y # Install CLI and configure detected agents\nbash \u003c(curl -fsSL https://railway.com/install.sh) # Shell script (macOS, Linux, Windows via WSL)\nnpm i -g @railway/cli # npm (macOS, Linux, Windows). Requires Node.js version 16 or higher.\nbrew install railway # Homebrew (macOS)","type":"text"}]},{"type":"paragraph","content":[{"text":"If not authenticated, see ","type":"text"},{"text":"Account creation & sign-in","type":"text","marks":[{"type":"link","attrs":{"href":"#account-creation--sign-in","title":null}}]},{"text":" below — the CLI offers unauthed ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" (deploy + sign up/in in one shot) or ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" (sign up/in only; new accounts created on the fly). If not linked and no URL was provided, run ","type":"text"},{"text":"railway link --project \u003cid-or-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"If a command is not recognized (for example, ","type":"text"},{"text":"railway environment edit","type":"text","marks":[{"type":"code_inline"}]},{"text":"), the CLI may be outdated. Upgrade with:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway upgrade","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Account creation & sign-in","type":"text"}]},{"type":"paragraph","content":[{"text":"Railway uses a single unified OAuth flow for both sign-in and sign-up. The backend detects fresh accounts from durable compliance state (a CLI client that hasn't accepted ToS / Fair Use yet) and adapts the consent screen and post-auth landing page — new users land on a \"Welcome to Railway!\" page, existing users see the standard confirmation. The CLI does not declare signup intent up front.","type":"text"}]},{"type":"paragraph","content":[{"text":"Two commands surface this flow, depending on intent:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"When to use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Agent-friendly onboarding from the current directory. Unauthenticated → opens the browser (or device-code) to sign in / sign up. With no linked project, a detected agent harness (or ","type":"text"},{"text":"-y","type":"text","marks":[{"type":"code_inline"}]},{"text":") auto-creates a project + service and deploys; an interactive human is offered create / link-existing / cancel. Add ","type":"text"},{"text":"-y","type":"text","marks":[{"type":"code_inline"}]},{"text":" to skip prompts and force the create non-interactively (works even if harness detection misses).","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sign in — ","type":"text"},{"text":"and","type":"text","marks":[{"type":"em"}]},{"text":" sign up. New accounts are created on the fly through the same OAuth surface; there is no separate signup command.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Related: ","type":"text"},{"text":"railway up --new","type":"text","marks":[{"type":"code_inline"}]},{"text":" creates a ","type":"text"},{"text":"fresh","type":"text","marks":[{"type":"em"}]},{"text":" project + service from the current directory and deploys it even if one is already linked (use when already signed in and the user wants a new app); ","type":"text"},{"text":"--name \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":" overrides the project name.","type":"text"}]},{"type":"paragraph","content":[{"text":"Choosing the path:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deploy from cwd → run ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" (interactive) or ","type":"text"},{"text":"railway up -y","type":"text","marks":[{"type":"code_inline"}]},{"text":" (skips the confirm prompt). Run it yourself; don't ask the user to sign in separately first.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"New project from cwd when already signed in → ","type":"text"},{"text":"railway up --new","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sign up with a deployable app in cwd → ","type":"text","marks":[{"type":"strong"}]},{"text":"railway up","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" (signs up ","type":"text"},{"text":"and","type":"text","marks":[{"type":"em"}]},{"text":" deploys — bare ","type":"text"},{"text":"up","type":"text","marks":[{"type":"code_inline"}]},{"text":" works for a detected agent, even if the user only said \"sign me up\"; add ","type":"text"},{"text":"-y","type":"text","marks":[{"type":"code_inline"}]},{"text":" to skip prompts / force it non-interactively). Sign in, or sign up with nothing to deploy → ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" (creates new accounts on the fly).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Headless / no browser:","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"The CLI ","type":"text"},{"text":"auto-detects","type":"text","marks":[{"type":"strong"}]},{"text":" SSH sessions, CI, and a missing ","type":"text"},{"text":"DISPLAY","type":"text","marks":[{"type":"code_inline"}]},{"text":" and switches to the device-code flow on its own — you almost never need to force it.","type":"text"}]},{"type":"paragraph","content":[{"text":"Do NOT pass ","type":"text","marks":[{"type":"strong"}]},{"text":"--browserless","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" just because you are an agent or your shell is non-interactive.","type":"text","marks":[{"type":"strong"}]},{"text":" If the human is at this machine (a local IDE or desktop session — the common case), bare ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" opens ","type":"text"},{"text":"their","type":"text","marks":[{"type":"em"}]},{"text":" browser directly, which completes far more reliably than relaying a device code (~90% vs ~60% success for agent-driven sign-ins). Being a coding agent does not make the machine headless.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway login --browserless # ONLY for machines with genuinely no browser","type":"text"}]},{"type":"paragraph","content":[{"text":"Forces the device-code flow (RFC 8628): prints a sign-in link and a short code for the user to open on any device. Reserve it for machines where no browser exists — SSH boxes, containers, remote VMs the auto-detection missed. When you do end up in a device-code flow, follow the relay procedure below: surface the sign-in link to the user the moment it prints.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent harness, human present","type":"text","marks":[{"type":"strong"}]},{"text":": when the CLI detects an agent harness (Claude Code, Cursor, Codex, …) with a human at the keyboard, ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" opens the browser and skips the confirm prompt — the agent invocation is treated as consent. A real human still has to complete OAuth in the browser.","type":"text"}]},{"type":"paragraph","content":[{"text":"Device-code sign-in: relay the link immediately (CRITICAL):","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"When the CLI can't open a browser (sandboxed shell, container, SSH, no ","type":"text"},{"text":"DISPLAY","type":"text","marks":[{"type":"code_inline"}]},{"text":"), unauthed ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":" print a sign-in URL + short code and then ","type":"text"},{"text":"block, polling for up to 10 minutes","type":"text","marks":[{"type":"strong"}]},{"text":" while the user completes sign-in. The code expires after 10 minutes. If you run this as a normal foreground command, your harness buffers the output until the command exits — ","type":"text"},{"text":"the user never sees the link until the code is already dead","type":"text","marks":[{"type":"strong"}]},{"text":". This is the #1 cause of failed agent-driven signups. Handle it like this:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preferred — background execution","type":"text","marks":[{"type":"strong"}]},{"text":" (e.g. Claude Code: ","type":"text"},{"text":"run_in_background","type":"text","marks":[{"type":"code_inline"}]},{"text":", then poll with ","type":"text"},{"text":"BashOutput","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start the command in the background.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Poll its output. The instant a sign-in block appears (","type":"text"},{"text":"Sign in with one click: \u003curl>","type":"text","marks":[{"type":"code_inline"}]},{"text":" on newer CLIs, or ","type":"text"},{"text":"Sign in at: \u003curl>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"Enter this code: \u003ccode>","type":"text","marks":[{"type":"code_inline"}]},{"text":" on older ones), ","type":"text"},{"text":"stop everything and relay it to the user verbatim","type":"text","marks":[{"type":"strong"}]},{"text":" — do not summarize, shorten, or defer it. Prefer the one-click URL when present; otherwise relay the URL and code together. Tell the user to open the link now.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Leave the command running and keep polling. When the user completes sign-in, the same process picks up the session and continues into the deploy on its own. Then verify per the deploy rules below.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No background support — set expectations, use the longest timeout:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before running, tell the user: ","type":"text"},{"text":"\"This will print a sign-in link — I'll show it to you the moment I have it. Please complete it promptly; the code expires in 10 minutes.\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run with the longest timeout your harness allows.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the command times out or is killed before sign-in completed, the printed code is ","type":"text"},{"text":"no longer being monitored","type":"text","marks":[{"type":"strong"}]},{"text":" — a late click does nothing. Relay whatever link appeared anyway for context, then immediately re-run the command and relay the ","type":"text"},{"text":"new","type":"text","marks":[{"type":"strong"}]},{"text":" link, telling the user to always use the newest one.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never","type":"text","marks":[{"type":"strong"}]},{"text":" wait silently for the command to finish before showing the link, and never report the sign-in as failed without first relaying the link and giving the user a chance to act.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The browser transport needs none of this — the CLI opens the browser on the user's machine itself.","type":"text"}]},{"type":"paragraph","content":[{"text":"JSON / CI modes do not auto-prompt","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"railway up --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"railway up --ci","type":"text","marks":[{"type":"code_inline"}]},{"text":" will NOT open a browser for an unauthed user. ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" emits a structured error instead:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\"error\":\"Not signed in.\",\"code\":\"NOT_AUTHENTICATED\",\"hint\":\"Run `railway login` to authenticate, then re-run.\"}","type":"text"}]},{"type":"paragraph","content":[{"text":"When you see ","type":"text"},{"text":"code: NOT_AUTHENTICATED","type":"text","marks":[{"type":"code_inline"}]},{"text":", authenticate the user with ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":", then retry the original command.","type":"text"}]},{"type":"paragraph","content":[{"text":"OAUTH_INSUFFICIENT_GRANT","type":"text","marks":[{"type":"code_inline"}]},{"text":" is different: the session is valid but lacks access to the resource. Check IDs, workspace membership, and the integration's grant scope instead of looping through login; see ","type":"text"},{"text":"operate.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/operate.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Fully unattended (no human at all)","type":"text","marks":[{"type":"strong"}]},{"text":": set ","type":"text"},{"text":"RAILWAY_API_TOKEN","type":"text","marks":[{"type":"code_inline"}]},{"text":" (account-scoped) or ","type":"text"},{"text":"RAILWAY_TOKEN","type":"text","marks":[{"type":"code_inline"}]},{"text":" (project-scoped) instead of running an interactive login. A brand-new user with no token and no human present cannot complete signup — there is no headless account-creation path.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Agent tooling","type":"text"}]},{"type":"paragraph","content":[{"text":"Use direct Railway CLI commands for deterministic operations. Use ","type":"text"},{"text":"railway agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" only when the user explicitly asks for Railway Agent, wants a natural-language investigation, or the task is broader than a single resource operation.","type":"text"}]},{"type":"paragraph","content":[{"text":"Set up Railway skills, MCP, and authentication with:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway setup agent\nrailway setup agent -y\nrailway setup agent --oauth","type":"text"}]},{"type":"paragraph","content":[{"text":"railway setup agent -y","type":"text","marks":[{"type":"code_inline"}]},{"text":" skips the interactive login flow. If the user isn't authenticated after setup, run ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Install or update MCP and skills directly when the user names a target tool:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway mcp install # hosted MCP via CLI login\nrailway mcp install --agent codex --oauth # direct HTTP, editor OAuth\nrailway mcp install --agent cursor --oauth\nrailway skills\nrailway skills update --agent codex\nrailway skills remove --agent cursor","type":"text"}]},{"type":"paragraph","content":[{"text":"Supported targets include ","type":"text"},{"text":"claude-code","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"cursor","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"codex","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"opencode","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"copilot","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"factory-droid","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install mode","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transport and authentication","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default / ","type":"text"},{"text":"--remote","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"railway mcp","type":"text","marks":[{"type":"code_inline"}]},{"text":" stdio proxy to hosted MCP, authenticated by ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--oauth","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct HTTP to ","type":"text"},{"text":"https://mcp.railway.com","type":"text","marks":[{"type":"code_inline"}]},{"text":", authenticated by editor OAuth; matches the published plugins","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--local","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"In-process GraphQL-backed stdio server, invoked as ","type":"text"},{"text":"railway mcp local","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"These modes apply to both ","type":"text"},{"text":"mcp install","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"setup agent","type":"text","marks":[{"type":"code_inline"}]},{"text":"; interactive setup offers a choice. ","type":"text"},{"text":"railway mcp proxy","type":"text","marks":[{"type":"code_inline"}]},{"text":" remains an alias for the default proxy. The proxy may fill only a linked project ID when the tool accepts it and the call supplies no resource scope. Continue passing explicit project, environment, and service IDs for scoped work.","type":"text"}]},{"type":"paragraph","content":[{"text":"Use Railway Agent chat with:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway agent\nrailway agent -p \"why is my service crashing?\"\nrailway agent -p \"summarize the deployment status\" --json\nrailway agent --list --json\nrailway agent --thread-id \u003cthread-id>","type":"text"}]},{"type":"paragraph","content":[{"text":"railway agent","type":"text","marks":[{"type":"code_inline"}]},{"text":" requires user OAuth authentication from ","type":"text"},{"text":"railway login","type":"text","marks":[{"type":"code_inline"}]},{"text":". Project tokens (","type":"text"},{"text":"RAILWAY_TOKEN","type":"text","marks":[{"type":"code_inline"}]},{"text":") are not supported for Railway Agent chat. If an agent command is unavailable, upgrade with ","type":"text"},{"text":"railway upgrade --yes","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common quick operations","type":"text"}]},{"type":"paragraph","content":[{"text":"These are frequent enough to handle without loading a reference. Use the matching MCP tool when the job is platform-scoped and the tool is available; otherwise use the CLI:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"railway status --json # current context\nrailway whoami --json # auth and workspace info\nrailway project list --json # list projects\nrailway service list --json # services in current environment (verify before retrying `add`)\nrailway add --database \u003ctype> --json # add one database; ALWAYS pass --json\nrailway add --service \u003cname> --json # add empty service; ALWAYS pass --json\nrailway variable list --service \u003csvc> --json # list variables\nrailway variable set KEY=value --service \u003csvc> # set a variable\nrailway domain list --service \u003csvc> --json # domains and DNS status\nrailway logs --service \u003csvc> --lines 200 --json # recent logs\nrailway logs --service \u003csvc> --network --lines 200 --json # network flow snapshot\nrailway metrics --service \u003csvc> --since 1h --json # resource and HTTP metrics summary\nrailway up --detach -m \"\u003csummary>\" # deploy current directory (returns at QUEUED — verify before reporting)\nrailway deployment list --json # poll newest deployment status after a detached up\nrailway bucket list --json # list buckets in current environment\nrailway bucket info --bucket \u003cname> --json # bucket storage and object count\nrailway bucket credentials --bucket \u003cname> --json # S3-compatible credentials","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Routing","type":"text"}]},{"type":"paragraph","content":[{"text":"For anything beyond quick operations, load the references needed for the user's intent. Most requests need one or two; compose more when the workflow crosses areas.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intent","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reference","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use for","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Analyze a database","type":"text","marks":[{"type":"strong"}]},{"text":" (\"analyze \u003curl>\", \"analyze db\", \"analyze database\", \"analyze service\", \"introspect\", \"check my postgres/redis/mysql/mongo\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze-db.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/analyze-db.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Database introspection and performance analysis. analyze-db.md directs you to the DB-specific reference. ","type":"text"},{"text":"This takes priority over the status/operate routes when a Railway URL to a database service is provided alongside \"analyze\".","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create or connect resources","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"setup.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/setup.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Projects, services, databases, buckets, templates, workspaces","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ship code or manage releases","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/deploy.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deploy, redeploy, restart, build config, monorepo, Dockerfile","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Change configuration (\"webhook\", \"notify on deploy\", \"custom header\", \"Slack/Discord notifications\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"configure.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/configure.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Environments, variables, config patches, domains, networking, project webhooks with the ","type":"text"},{"text":"list-webhooks","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"create-webhook","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-webhook","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"test-webhook","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"delete-webhook","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP tools and their custom headers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manage feature flags","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"feature-flags.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/feature-flags.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MCP registry operations; CLI targeting rules and rollouts; SDK runtime reads","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Define configuration in source control (\"IaC\", \"infrastructure as code\", \"config as code\", ","type":"text"},{"text":".railway/railway.ts","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".railway/railway.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".railway/railway.go","type":"text","marks":[{"type":"code_inline"}]},{"text":", \"config migrate/plan/apply/pull\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"iac.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/iac.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Author/import IaC, migrate legacy JSON/TOML, save and apply reviewed plans, check drift","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Manage databases (\"PITR\", \"restore\", \"backup\", \"HA\", \"failover\", \"switchover\", \"PgBouncer\", \"connection pooling\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"databases.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/databases.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Postgres recovery, HA and pooling; MySQL/Redis HA; use analysis references for performance investigations","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Inspect costs or manage spending limits","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"usage.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/usage.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Workspace/project/service usage, billing periods, workspace and Railway Agent limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run a coding agent on Railway (\"cloud agent\", \"railway ca\", \"railway code\", \"desktop SSH\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cloud-agents.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cloud-agents.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Provision, connect, wake, sleep, delete, or configure desktop access to cloud agent VMs","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check health or debug failures","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"operate.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/operate.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status, logs, metrics, build/runtime triage, recovery","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Trace requests across services (\"tracing\", \"traces\", \"trace ID\", \"spans\", \"OpenTelemetry\", \"OTel\", \"OTLP\", \"instrument my app\", \"instrument my function\", \"Bun function\", \"auto-instrumentation\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"tracing.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/tracing.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enable tracing per service and environment with the ","type":"text"},{"text":"get-tracing","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"set-service-tracing","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP tools or ","type":"text"},{"text":"railway trace enable","type":"text","marks":[{"type":"code_inline"}]},{"text":", the IaC ","type":"text"},{"text":"tracing","type":"text","marks":[{"type":"code_inline"}]},{"text":" block (SDK and CLI version gates), SDK instrumentation (preferred) vs automatic (eBPF), what to instrument, instrumenting a Function (Bun), the provided ","type":"text"},{"text":"OTEL_*","type":"text","marks":[{"type":"code_inline"}]},{"text":" variables, sampling, reading traces with the ","type":"text"},{"text":"list-traces","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get-trace","type":"text","marks":[{"type":"code_inline"}]},{"text":" MCP tools or ","type":"text"},{"text":"railway trace list","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get","type":"text","marks":[{"type":"code_inline"}]},{"text":", checking what a service's instrumentation covers with ","type":"text"},{"text":"get-tracing-coverage","type":"text","marks":[{"type":"code_inline"}]},{"text":", the Traces tab","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use a sandbox or build remotely (\"sandbox\", \"scratch environment\", \"ephemeral box\", \"build remotely\", \"remote build\", \"run this remotely\", \"checkpoint\", \"snapshot/save/restore sandbox state\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sandbox.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sandbox.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create/fork sandboxes, run commands remotely, remote template builds, checkpoints (save/restore sandbox state), port forwarding, teardown. Requires Sandboxes enabled in Priority Boarding — if unavailable, prompt the user to enable it.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Request from API, docs, or community","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"request.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/request.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Railway GraphQL API queries/mutations, metrics queries, Central Station, official docs","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"If the request spans two areas (for example, \"deploy and then check if it's healthy\"), load both references and compose one response.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Execution rules","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use Railway CLI for workflows that need the current repo, local shell, SSH, database scripts, local Railway context, or exact command output.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use Remote MCP for OAuth-scoped platform operations that match an available MCP tool and do not need local files or CLI state.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use local CLI MCP only when the current agent already has it explicitly configured and it exposes a needed operation not available through Remote MCP.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"railway api","type":"text","marks":[{"type":"code_inline"}]},{"text":" for operations without a dedicated MCP tool or CLI command; retain the legacy helper only for CLI compatibility.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"--json","type":"text","marks":[{"type":"code_inline"}]},{"text":" output where available for reliable parsing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolve context before mutation. Know which project, environment, and service you're acting on.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For destructive actions (delete service, remove deployment, drop database), confirm intent and state impact before executing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After mutations, verify the result with a read-back command or MCP read.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never report a deploy as successful without observing SUCCESS for that deployment.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"up --detach","type":"text","marks":[{"type":"code_inline"}]},{"text":", a non-TTY ","type":"text"},{"text":"up","type":"text","marks":[{"type":"code_inline"}]},{"text":" without CI mode, or a timed-out stream may return after upload. Follow the deployment ID from the upload in ","type":"text"},{"text":"railway deployment list --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the same project/environment/service scope; do not substitute a concurrent newer deployment. If status is ","type":"text"},{"text":"FAILED","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"CRASHED","type":"text","marks":[{"type":"code_inline"}]},{"text":", triage per ","type":"text"},{"text":"operate.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/operate.md","title":null}}]},{"text":". If status is ","type":"text"},{"text":"NEEDS_APPROVAL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SLEEPING","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SKIPPED","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"REMOVED","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"REMOVING","type":"text","marks":[{"type":"code_inline"}]},{"text":", or unknown, report that state and the next action. Exit 0 alone is insufficient; see ","type":"text"},{"text":"deploy.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/deploy.md","title":null}}]},{"text":" for CI streaming and polling.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"User-only commands (NEVER execute directly)","type":"text"}]},{"type":"paragraph","content":[{"text":"These commands modify database state and require the user to run them directly in their terminal. ","type":"text"},{"text":"Do NOT execute these with Bash. Instead, show the command and ask the user to run it.","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Why user-only","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python3 scripts/enable-pg-stats.py --service \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Modifies shared_preload_libraries, may restart database","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python3 scripts/pg-extensions.py --service \u003cname> install \u003cext>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Installs database extension","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python3 scripts/pg-extensions.py --service \u003cname> uninstall \u003cext>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Removes database extension","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ALTER SYSTEM SET ...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Changes PostgreSQL configuration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DROP EXTENSION ...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Removes database extension","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CREATE EXTENSION ...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Installs database extension","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"When these operations are needed:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explain what the command does and any side effects (e.g., restart required)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Show the exact command the user must run","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Wait for user confirmation that they ran it","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify the result with a read-only query","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Composition patterns","type":"text"}]},{"type":"paragraph","content":[{"text":"Multi-step workflows follow natural chains:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add object storage","type":"text","marks":[{"type":"strong"}]},{"text":": setup (create bucket), setup (get credentials), configure (set S3 variables on app service)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"First deploy","type":"text","marks":[{"type":"strong"}]},{"text":": setup (create project + service), configure (set variables and source), deploy, operate (verify healthy)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix a failure","type":"text","marks":[{"type":"strong"}]},{"text":": operate (triage logs), configure (fix config/variables), deploy (redeploy), operate (verify recovery)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add a domain","type":"text","marks":[{"type":"strong"}]},{"text":": configure (add domain + set port), operate (verify DNS and service health)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add tracing","type":"text","marks":[{"type":"strong"}]},{"text":": tracing (enable for the service in the right environment with ","type":"text"},{"text":"set-service-tracing","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"railway trace enable","type":"text","marks":[{"type":"code_inline"}]},{"text":", prefer SDK instrumentation over automatic, add spans around inbound work, I/O and logical units; for a Function, edit its file with ","type":"text"},{"text":"get-function-source-code","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"update-function-source-code","type":"text","marks":[{"type":"code_inline"}]},{"text":"), configure (set ","type":"text"},{"text":"OTEL_METRICS_EXPORTER","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"OTEL_LOGS_EXPORTER","type":"text","marks":[{"type":"code_inline"}]},{"text":", start command), deploy (redeploy so the ","type":"text"},{"text":"OTEL_*","type":"text","marks":[{"type":"code_inline"}]},{"text":" variables land), tracing (verify with ","type":"text"},{"text":"x-railway-trace-id","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"get-trace","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"railway trace get","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"get-tracing-coverage","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the database and client spans)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Docs to action","type":"text","marks":[{"type":"strong"}]},{"text":": request (fetch docs answer), route to the relevant operational reference","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When composing, return one unified response covering all steps. Don't ask the user to invoke each step separately.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Setup decision flow","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user wants to create or deploy something, determine the right action from current context:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the intent is deploy-from-cwd or signup-from-cwd, skip ","type":"text"},{"text":"railway whoami","type":"text","marks":[{"type":"code_inline"}]},{"text":" and run ","type":"text"},{"text":"railway up","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"railway up -y","type":"text","marks":[{"type":"code_inline"}]},{"text":") directly per ","type":"text"},{"text":"Intent-based routing","type":"text","marks":[{"type":"link","attrs":{"href":"#intent-based-routing","title":null}}]},{"text":" — it handles signup, project creation, service creation, and deploy in one chain. For other setup flows that need workspace/account context first, run ","type":"text"},{"text":"railway whoami --json","type":"text","marks":[{"type":"code_inline"}]},{"text":"; if it fails with an auth error the user has no token — route through ","type":"text"},{"text":"Account creation & sign-in","type":"text","marks":[{"type":"link","attrs":{"href":"#account-creation--sign-in","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the current directory.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If linked","type":"text","marks":[{"type":"strong"}]},{"text":": add a service to the existing project (","type":"text"},{"text":"railway add --service \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Do not create a new project unless the user explicitly says \"new project\" or \"separate project\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If not linked","type":"text","marks":[{"type":"strong"}]},{"text":": check the parent directory (","type":"text"},{"text":"cd .. && railway status --json","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parent linked","type":"text","marks":[{"type":"strong"}]},{"text":": this is likely a monorepo sub-app. Add a service and set ","type":"text"},{"text":"rootDirectory","type":"text","marks":[{"type":"code_inline"}]},{"text":" to the sub-app path.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parent not linked","type":"text","marks":[{"type":"strong"}]},{"text":": run ","type":"text"},{"text":"railway list --json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and look for a project matching the directory name.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Match found","type":"text","marks":[{"type":"strong"}]},{"text":": link to it (","type":"text"},{"text":"railway link --project \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No match","type":"text","marks":[{"type":"strong"}]},{"text":": create a new project (","type":"text"},{"text":"railway init --name \u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When multiple workspaces exist, match by name from ","type":"text"},{"text":"railway whoami --json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Naming heuristic","type":"text","marks":[{"type":"strong"}]},{"text":": app names like \"flappy-bird\" or \"my-api\" are service names, not project names. Use the directory or repo name for the project.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Response format","type":"text"}]},{"type":"paragraph","content":[{"text":"For all operational responses, return:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What was done (action and scope).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The result (IDs, status, key output).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"What to do next (or confirmation that the task is complete).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Keep output concise. Include command evidence only when it helps the user understand what happened.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"use-railway","author":"@skillopedia","source":{"stars":340,"repo_name":"railway-skills","origin_url":"https://github.com/railwayapp/railway-skills/blob/HEAD/plugins/railway/skills/use-railway/SKILL.md","repo_owner":"railwayapp","body_sha256":"de7f6dc86d247a148fe2aa71a363867ca1bde458b071189573d750589c00f079","cluster_key":"366ca9b04d5f4672a35bc3d1dc3a16fed42df7722d0908d8a9fa64eab1b2cf46","clean_bundle":{"format":"clean-skill-bundle-v1","source":"railwayapp/railway-skills/plugins/railway/skills/use-railway/SKILL.md","text_bytes":585951,"source_commit":"526e95524e964ed326689cbac4464d97a58ab81c","skills_sh_rank":2718,"attachment_count":25,"text_attachments":25,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"plugins/railway/skills/use-railway/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"devops-infrastructure","import_tag":"skills-sh-top20k-2026-11","description":"Operate Railway infrastructure: sign up for or sign in to a Railway account, create projects, provision services, databases, and buckets, deploy code, configure infrastructure as code, environments and variables, manage domains, trace requests with OpenTelemetry, troubleshoot failures, check status and metrics, manage feature flags, database recovery and HA, cloud agents, usage limits, and Railway agent tooling. Use this skill whenever the user mentions Railway, feature flags, flag rollout, targeting rules, signing up, creating an account, registering, logging in, deployments, services, environments, buckets, object storage, tracing, traces, spans, OpenTelemetry, OTLP, build failures, agent setup, MCP, or infrastructure operations, even if they don't say \"Railway\" explicitly. Also invoke it when the user asks to be signed up, registered, or onboarded: do not refuse — drive them through the unauthed `railway up` flow (deploys + signs up on the fly) or `railway login` (which creates accounts on the fly).\n","allowed-tools":"Bash(railway:*), Bash(which:*), Bash(command:*), Bash(npm:*), Bash(npx:*), Bash(curl:*), Bash(python3:*)"}},"renderedAt":1793666494497}

Use Railway Railway resource model Railway organizes infrastructure in a hierarchy: - Workspace is the billing and team scope. A user belongs to one or more workspaces. - Project is a collection of services under one workspace. It maps to one deployable unit of work. - Environment is an isolated configuration plane inside a project (for example, , ). Each environment has its own variables, config, and deployment history. - Service is a single deployable unit inside a project. It can be an app from a repo, a Docker image, or a managed database. - Bucket is an S3-compatible object storage resou…