2025-03-13 02:09:33 +00:00
|
|
|
use std::cmp::Ordering;
|
|
|
|
|
|
2023-03-04 02:44:32 +00:00
|
|
|
use anyhow::Result;
|
2025-04-30 18:29:22 +00:00
|
|
|
use clap::error::ErrorKind;
|
2023-03-04 02:44:32 +00:00
|
|
|
|
|
|
|
|
mod commands;
|
|
|
|
|
use commands::*;
|
2026-03-25 18:38:18 +00:00
|
|
|
use config::Configs;
|
2025-03-13 02:09:33 +00:00
|
|
|
use is_terminal::IsTerminal;
|
2025-08-05 01:39:39 +00:00
|
|
|
use util::{check_update::UpdateCheck, compare_semver::compare_semver};
|
2023-03-04 02:44:32 +00:00
|
|
|
|
|
|
|
|
mod client;
|
|
|
|
|
mod config;
|
|
|
|
|
mod consts;
|
2023-03-08 18:25:17 +00:00
|
|
|
mod controllers;
|
2023-04-26 13:26:40 +00:00
|
|
|
mod errors;
|
2023-03-04 02:44:32 +00:00
|
|
|
mod gql;
|
2026-03-25 18:38:18 +00:00
|
|
|
mod oauth;
|
2023-03-04 02:44:32 +00:00
|
|
|
mod subscription;
|
|
|
|
|
mod table;
|
|
|
|
|
mod util;
|
2025-03-25 17:02:11 +00:00
|
|
|
mod workspace;
|
2023-03-04 02:44:32 +00:00
|
|
|
|
|
|
|
|
#[macro_use]
|
|
|
|
|
mod macros;
|
Add per-command CLI telemetry (#791)
* Add per-command telemetry via cliEventTrack mutation
Sends command name, subcommand, duration, success/error, OS, arch,
CLI version, and CI flag to the Railway API after each invocation.
Silently skipped when unauthenticated, on network failure, or when
the user opts out via DO_NOT_TRACK=1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Migrate telemetry to typed graphql_client mutation
Pull updated schema with cliEventTrack mutation and replace the raw
JSON POST with typed codegen via graphql_client, matching the rest
of the codebase. Also adds DNS_RECORD_TYPE_TXT from the schema update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add one-time telemetry notice on first run
Shows a notice to stderr informing users about CLI usage data
collection, with DO_NOT_TRACK=1 opt-out. Persisted to
~/.railway/notices.json so it only displays once. Suppressed
in CI/non-TTY and when telemetry is already disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Support RAILWAY_NO_TELEMETRY env var for opting out of telemetry
Adds RAILWAY_NO_TELEMETRY=1 as a Railway-specific alternative to
DO_NOT_TRACK=1 for disabling CLI telemetry. Updates the notice
text to mention both variables.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix telemetry docs link to /cli/telemetry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix cargo fmt --all formatting in telemetry notice
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:03:59 +00:00
|
|
|
mod telemetry;
|
2023-03-04 02:44:32 +00:00
|
|
|
|
|
|
|
|
// Generates the commands based on the modules in the commands directory
|
|
|
|
|
// Specify the modules you want to include in the commands_enum! macro
|
2025-04-30 18:29:22 +00:00
|
|
|
commands!(
|
2023-03-04 02:44:32 +00:00
|
|
|
add,
|
2026-03-06 19:34:03 +00:00
|
|
|
bucket,
|
2023-03-04 02:44:32 +00:00
|
|
|
completion,
|
2023-03-23 23:15:49 +00:00
|
|
|
connect,
|
2026-01-08 19:44:27 +00:00
|
|
|
delete,
|
2024-06-25 16:52:12 +00:00
|
|
|
deploy,
|
2025-09-26 19:22:46 +00:00
|
|
|
deployment,
|
Add dev command for local development with Docker Compose (#710)
* init develop command
* develop: use service names, random ports, project-specific dir
- extract environment config types to controllers/environment_config.rs
- use slugified service names instead of IDs in compose
- generate deterministic external ports (10000-60000) to avoid conflicts
- save compose to ~/.railway/develop/<project_id>/
- always run detached
* develop: show service names and URLs in output
* develop: resolve variables and override Railway networking vars
- Fetch resolved variables via VariablesForServiceDeployment API
- Override RAILWAY_PRIVATE_DOMAIN with docker-compose service slug
- Override RAILWAY_PUBLIC_DOMAIN and RAILWAY_TCP_PROXY_DOMAIN with localhost
- Override RAILWAY_TCP_PROXY_PORT with mapped local port
- Replace *.railway.internal refs in values with service slugs
- Filter deprecated vars (RAILWAY_STATIC_URL, RAILWAY_SERVICE_*_URL)
- Add docker-compose networking for inter-container communication
- Escape $ as $$ in commands for docker-compose interpolation
* develop: add volume support and down subcommand
- Add volumes field to docker-compose output (persist through up/down)
- Add `develop down` subcommand to stop containers
- Use environment ID for compose dir and volume names
- Show volume mount paths in service summary
* run: use local services when develop is active
Add --no-local flag to skip. Extracts shared variable override logic
into controllers/local_override.rs for reuse between run and develop.
* develop down: add --clean flag to remove volumes and files
* restart services on failure
* develop: add local HTTPS with mkcert and pretty URLs
- auto-generate TLS certs via mkcert for {project}.railway.dev
- add Caddy reverse proxy for HTTPS termination
- set RAILWAY_PUBLIC_DOMAIN to https://{domain}:{port}
- auto-add /etc/hosts entry on develop up
- remove hosts entry on develop down --clean
- skip cert generation if already exists
* develop: use .localhost TLD instead of .railway.dev
.localhost resolves to 127.0.0.1 per RFC 6761, eliminating need for
/etc/hosts manipulation and sudo prompts
* process manager
* develop: support port 443 for prettier public URLs
- Try binding to port 443 at startup for cleaner URLs
- Port 443 mode: https://{service}.{project}.railway.localhost
- Fallback mode: https://{project}.railway.localhost:{port}
- Generate wildcard certs (*.project.railway.localhost) for port 443
- SNI-based routing in Caddy when using port 443
- Add http:// prefix to private domain display
* develop: add session lock to prevent concurrent code service runs
- Add DevelopSessionLock using fs2 file locking to prevent multiple
develop sessions running code services for the same environment
- Check existing https_mode before falling back from port 443
- Lock auto-releases on drop or process crash
* develop: refactor into testable module structure
- Extract shared types to controllers/config/ (EnvironmentConfig)
- Create controllers/develop/ with submodules:
- ports.rs: slugify, generate_port + tests
- variables.rs: override_railway_vars + tests
- session.rs: DevelopSessionLock + tests
- traits.rs: EnvironmentDataProvider, CommandRunner + mocks
- compose.rs: DockerCompose types, build_port_infos
- https_proxy.rs: HttpsConfig, certs, Caddyfile gen
- code_runner.rs: ProcessManager
- local_config.rs: LocalDevConfig
- Add async-trait dependency for trait abstractions
- Old controller files re-export for backward compat
* develop: remove unused traits and cleanup backward compat stubs
- Delete unused EnvironmentDataProvider and CommandRunner traits (371 lines)
- Add fetch_environment_config() function to controllers/config
- Update develop.rs to use centralized fetch function
- Delete empty stub files: process_manager.rs, develop_lock.rs,
local_https.rs, local_dev_config.rs
- Update local_override.rs to import directly from develop module
* develop: remove unused get_env_vars, cleanup dead_code attributes
- Remove unused get_env_vars method from ServiceInstance
- Remove unnecessary #![allow(dead_code)] from compose.rs and output.rs
- Add explanatory comment for dead_code allow in environment.rs
(needed for API deserialization struct fields)
* dev: rename develop to dev, add clean subcommand
- rename command from `develop` to `dev` (with `develop` alias)
- extract `--clean` flag from `down` into separate `clean` subcommand
- update output: remove "Using port 443" msg, add checkmark to "Started X image services"
* dev: store data in ~/.railway/develop/{projectId} instead of environmentId
* remove unnecessary comments
* dev: add unit tests for compose, https_proxy, local_config
Tests for port building, caddyfile generation, cert existence, and config operations.
* dev: fix cross-platform compatibility for Linux/Windows
- Add extra_hosts to Caddy service for host.docker.internal on Linux
- Use shell execution (sh -c / cmd /C) for proper command parsing
- Replace path unwraps with to_string_lossy()
* dev: check docker compose availability before running
* fix: handle Windows error kind for file lock conflicts
* dev configure: add service menu and --remove <name> support
* dev: clarify comments in up_command
* fix: use fs2 lock_contended_error for cross-platform lock check
* fix: use privateNetworkEndpoint for private domain resolution
Previously used slugified service name which may not match the actual
private network endpoint configured in the environment.
* refactor: extract build_service_endpoints helper for privateNetworkEndpoint
DRYs up duplicate logic between dev.rs and local_override.rs.
Fixes run command using environment_id instead of project_id for
is_local_develop_active check.
* dev: improve empty state messages
Distinguish "no services at all" from "no code services" so users
get actionable guidance (railway add vs railway develop configure)
* fix: only show networking for code services when port configured
* fix: skip canonicalize on windows to avoid UNC path prefix
canonicalize() returns \\?\C:\... paths on Windows which cmd.exe rejects
* dev: improve empty state messages with color
* dev: show command as first log line for code services
* dev: add port selection to configure and detect port conflicts
- prompt for port during service configuration (default: inferred from Railway)
- skip port prompt if service has no networking config
- detect and warn about port conflicts during configure
- force reconfigure conflicting ports during `railway dev up`
- only set PORT env var for services with networking configured
* dev: allow configuring multiple services on first-time setup
Use multi-select prompt instead of single-select, with summary at end
* dev: show OS-specific Docker install URL
* dev: format Docker not found message
* remove test dir
* remove test and to gitignore
* dev: fix cross-service public domain replacement
When service A references service B's RAILWAY_PUBLIC_DOMAIN via variable
interpolation, the production domain wasn't being replaced with the local
equivalent. Now builds a mapping of production -> local public domains
and replaces them in all variable values.
* dev: show next steps when only image services running
* dev: extract constants and refactor up_command
- Add port range constants (PORT_RANGE_MIN, PORT_RANGE_SIZE, RANDOM_PORT_MIN, RANDOM_PORT_MAX)
- Add resolve_path() for cross-platform path canonicalization
- Move generate_random_port() to ports.rs
- Extract helper functions from up_command:
- print_image_service_summary()
- print_code_service_summary()
- build_public_domain_mapping()
- build_image_service_compose()
- setup_caddy_proxy()
2025-12-12 23:18:58 +00:00
|
|
|
dev(develop),
|
2023-03-04 02:44:32 +00:00
|
|
|
domain,
|
|
|
|
|
docs,
|
2023-04-08 00:49:34 +00:00
|
|
|
down,
|
2024-09-25 15:52:21 +00:00
|
|
|
environment(env),
|
2023-03-04 02:44:32 +00:00
|
|
|
init,
|
|
|
|
|
link,
|
|
|
|
|
list,
|
|
|
|
|
login,
|
|
|
|
|
logout,
|
|
|
|
|
logs,
|
Adds an experimental MCP server (#813)
* Add GraphQL queries and mutations for MCP server
Add Metrics and Templates queries for service observability and template
search. Add ServiceInstanceUpdate mutation for updating service settings
like build command, replicas, and health check path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Extract upload logic from up command into controller
Move tarball creation and upload logic into src/controllers/upload.rs so
it can be reused by both the up CLI command and the MCP deploy tool.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Change log callbacks from Fn to FnMut
The MCP log handler collects logs into a Vec, which requires mutating
captured state. Fn closures don't allow this — FnMut does. Existing
callers are unaffected since every Fn is a valid FnMut.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add MCP server for AI-agent access to Railway
Expose Railway operations (deploy, logs, variables, domains, templates,
metrics, etc.) as MCP tools so AI agents can manage infrastructure
programmatically. Uses the rmcp crate for the MCP protocol.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add more fields to update_service MCP tool
Expose cron_schedule, dockerfile_path, healthcheck_timeout,
restart_policy_type, restart_policy_max_retries, pre_deploy_command,
region, railway_config_file, and watch_patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix MCP server: deploy resolution, linked-context precedence, and workspace auto-detect
- Return clear error with available services when deploy gets an unknown service name
- Prefer locally-linked project over token-derived project in MCP context resolution
- Auto-detect team workspace when create_project is called without workspace_id
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix docs_search and split into search + fetch tools
- Replace GitHub API (401 auth failures) with public sitemap for page listing
- Fix content path from src/docs/ to content/ to match actual repo structure
- Use plain HTTP client for external fetches (Railway auth headers caused GitHub 401s)
- Split into docs_search (returns top 5 URLs) and docs_fetch (reads full page)
- Improve scoring: exact path segment matches rank higher than substrings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Hide confirm field from MCP schema to prevent AI agents from auto-setting it
AI agents were reading "Requires confirm: true" in tool descriptions and
auto-populating confirm on the first call, bypassing the destructive action
safety guard. Fix by adding #[schemars(skip)] to the confirm field on
RemoveServiceParams, RemoveBucketParams, and RemoveVolumeParams so it's
hidden from the JSON schema (serde still deserializes it, defaulting to
false). Updated descriptions to say "Returns a preview first" instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Reject create_service when both source_repo and source_image are provided
The backend silently picks repo over image when both are set (via an
if/else chain in the serviceCreate resolver), dropping the image with
no error or warning. This is confusing for MCP agents that may not
realize one source was ignored. Validate early in the MCP layer to
give a clear, actionable error instead of silent precedence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add help text for mcp command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Drop vulnerable time crate by disabling unused serde_with features
serde_with was pulling in the time crate (RUSTSEC-2026-0009, DoS via
stack exhaustion in RFC 2822 parsing) through default features. We only
use skip_serializing_none from the macros feature, so disable defaults
and enable only macros. This removes time and several other unused
transitive deps (hex, indexmap v1, deranged, powerfmt, num-conv).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 06:10:03 +00:00
|
|
|
mcp,
|
2023-03-04 02:44:32 +00:00
|
|
|
open,
|
feat: restructure CLI to object-action pattern (#761)
* feat: restructure CLI to object-action pattern
- Add service subcommands: logs, restart, scale (delegates to existing commands)
- Add project object with list, link, delete subcommands
- Add environment link subcommand
- Restructure variable command with list, set, delete subcommands
- Add VariableDelete GraphQL mutation
- All existing commands continue to work via aliases for backwards compat
* test: add CLI parsing tests for backwards compat and new commands
* refactor: DRY up variable.rs, fix scale arg detection, fix env output order
- Add ServiceContext helper to dedupe project/env/service resolution
- delete_variable now requires service (consistent w/ list/set)
- scale.rs: position-based arg check prevents false triggers
- environment link: output after successful write()
* fix: remove dead code, add service redeploy, fix variable stdin
- service.rs: remove dead code in link_command (duplicate args.service check)
- service.rs: add redeploy subcommand routing
- variable.rs: accept string for set cmd to support --stdin with key-only
- main.rs: add more CLI parsing tests
* fix: service link case-insensitive, multi-var set, spinner cleanup
- service link: restore case-insensitive name/id matching
- variable set: support multiple KEY=val args
- use create_spinner_if helper for json mode
- add tests for multi-var set
* refactor: simplify service link flow
* fix: resolve clippy too_many_arguments warnings
Introduce FetchLogsParams struct to reduce argument count
2026-01-14 18:31:04 +00:00
|
|
|
project,
|
2026-01-08 19:44:33 +00:00
|
|
|
run(local),
|
2023-03-04 02:44:32 +00:00
|
|
|
service,
|
|
|
|
|
shell,
|
2025-03-07 15:10:07 +00:00
|
|
|
ssh,
|
2023-03-04 02:44:32 +00:00
|
|
|
starship,
|
|
|
|
|
status,
|
2026-02-23 17:46:22 +00:00
|
|
|
telemetry_cmd(telemetry),
|
2023-03-04 02:44:32 +00:00
|
|
|
unlink,
|
|
|
|
|
up,
|
2026-01-08 20:12:15 +00:00
|
|
|
upgrade,
|
feat: restructure CLI to object-action pattern (#761)
* feat: restructure CLI to object-action pattern
- Add service subcommands: logs, restart, scale (delegates to existing commands)
- Add project object with list, link, delete subcommands
- Add environment link subcommand
- Restructure variable command with list, set, delete subcommands
- Add VariableDelete GraphQL mutation
- All existing commands continue to work via aliases for backwards compat
* test: add CLI parsing tests for backwards compat and new commands
* refactor: DRY up variable.rs, fix scale arg detection, fix env output order
- Add ServiceContext helper to dedupe project/env/service resolution
- delete_variable now requires service (consistent w/ list/set)
- scale.rs: position-based arg check prevents false triggers
- environment link: output after successful write()
* fix: remove dead code, add service redeploy, fix variable stdin
- service.rs: remove dead code in link_command (duplicate args.service check)
- service.rs: add redeploy subcommand routing
- variable.rs: accept string for set cmd to support --stdin with key-only
- main.rs: add more CLI parsing tests
* fix: service link case-insensitive, multi-var set, spinner cleanup
- service link: restore case-insensitive name/id matching
- variable set: support multiple KEY=val args
- use create_spinner_if helper for json mode
- add tests for multi-var set
* refactor: simplify service link flow
* fix: resolve clippy too_many_arguments warnings
Introduce FetchLogsParams struct to reduce argument count
2026-01-14 18:31:04 +00:00
|
|
|
variable(variables, vars, var),
|
2024-06-03 13:55:34 +00:00
|
|
|
whoami,
|
2024-06-07 21:32:31 +00:00
|
|
|
volume,
|
2025-03-13 02:09:33 +00:00
|
|
|
redeploy,
|
2026-01-08 21:35:58 +00:00
|
|
|
restart,
|
2025-04-30 18:29:22 +00:00
|
|
|
scale,
|
2025-07-31 00:06:13 +00:00
|
|
|
check_updates,
|
|
|
|
|
functions(function, func, fn, funcs, fns)
|
2023-03-04 02:44:32 +00:00
|
|
|
);
|
|
|
|
|
|
2025-08-05 01:39:39 +00:00
|
|
|
fn spawn_update_task() -> tokio::task::JoinHandle<anyhow::Result<Option<String>>> {
|
2025-03-13 02:09:33 +00:00
|
|
|
tokio::spawn(async move {
|
2025-11-25 03:58:38 +00:00
|
|
|
// outputting would break json output on CI
|
2025-03-13 02:09:33 +00:00
|
|
|
if !std::io::stdout().is_terminal() {
|
2025-08-05 01:39:39 +00:00
|
|
|
anyhow::bail!("Stdout is not a terminal");
|
2025-03-13 02:09:33 +00:00
|
|
|
}
|
2025-08-05 01:39:39 +00:00
|
|
|
let latest_version = util::check_update::check_update(false).await?;
|
2025-03-13 02:09:33 +00:00
|
|
|
|
2025-08-05 01:39:39 +00:00
|
|
|
Ok(latest_version)
|
2025-03-13 02:09:33 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 01:39:39 +00:00
|
|
|
async fn handle_update_task(
|
|
|
|
|
handle: Option<tokio::task::JoinHandle<anyhow::Result<Option<String>>>>,
|
|
|
|
|
) {
|
2025-03-13 02:09:33 +00:00
|
|
|
if let Some(handle) = handle {
|
|
|
|
|
match handle.await {
|
|
|
|
|
Ok(Ok(_)) => {} // Task completed successfully
|
|
|
|
|
Ok(Err(e)) => {
|
|
|
|
|
if !std::io::stdout().is_terminal() {
|
|
|
|
|
eprintln!("Failed to check for updates (not fatal)");
|
2025-08-04 20:52:24 +00:00
|
|
|
eprintln!("{e}");
|
2025-03-13 02:09:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Check Updates: Task panicked or failed to execute.");
|
2025-08-04 20:52:24 +00:00
|
|
|
eprintln!("{e}");
|
2025-03-13 02:09:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-04 02:44:32 +00:00
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() -> Result<()> {
|
2025-04-30 18:29:22 +00:00
|
|
|
let args = build_args().try_get_matches();
|
2025-08-05 01:39:39 +00:00
|
|
|
let check_updates_handle = if std::io::stdout().is_terminal() {
|
|
|
|
|
let update = UpdateCheck::read().unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
if let Some(latest_version) = update.latest_version {
|
|
|
|
|
if matches!(
|
|
|
|
|
compare_semver(env!("CARGO_PKG_VERSION"), &latest_version),
|
|
|
|
|
Ordering::Less
|
|
|
|
|
) {
|
|
|
|
|
println!(
|
|
|
|
|
"{} v{} visit {} for more info",
|
|
|
|
|
"New version available:".green().bold(),
|
|
|
|
|
latest_version.yellow(),
|
|
|
|
|
"https://docs.railway.com/guides/cli".purple(),
|
|
|
|
|
);
|
2025-03-13 02:09:33 +00:00
|
|
|
}
|
2025-08-05 01:39:39 +00:00
|
|
|
let update = UpdateCheck {
|
|
|
|
|
last_update_check: Some(chrono::Utc::now()),
|
|
|
|
|
latest_version: None,
|
|
|
|
|
};
|
|
|
|
|
update
|
|
|
|
|
.write()
|
|
|
|
|
.context("Failed to save time since last update check")?;
|
2025-03-13 02:09:33 +00:00
|
|
|
}
|
2025-08-05 01:39:39 +00:00
|
|
|
|
|
|
|
|
Some(spawn_update_task())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2023-03-04 02:44:32 +00:00
|
|
|
|
2025-03-13 02:09:33 +00:00
|
|
|
// https://github.com/clap-rs/clap/blob/cb2352f84a7663f32a89e70f01ad24446d5fa1e2/clap_builder/src/error/mod.rs#L210-L215
|
2025-04-30 18:29:22 +00:00
|
|
|
let cli = match args {
|
2025-03-13 02:09:33 +00:00
|
|
|
Ok(args) => args,
|
|
|
|
|
// Clap's source code specifically says that these errors should be
|
|
|
|
|
// printed to stdout and exit with a status of 0.
|
|
|
|
|
Err(e) if e.kind() == ErrorKind::DisplayHelp || e.kind() == ErrorKind::DisplayVersion => {
|
2025-08-04 20:52:24 +00:00
|
|
|
println!("{e}");
|
2025-03-13 02:09:33 +00:00
|
|
|
handle_update_task(check_updates_handle).await;
|
|
|
|
|
std::process::exit(0);
|
|
|
|
|
}
|
2023-04-26 13:26:40 +00:00
|
|
|
Err(e) => {
|
2025-08-04 20:52:24 +00:00
|
|
|
eprintln!("{e}");
|
2025-03-13 02:09:33 +00:00
|
|
|
handle_update_task(check_updates_handle).await;
|
|
|
|
|
std::process::exit(2); // The default behavior is exit 2
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-04-26 13:26:40 +00:00
|
|
|
|
2026-03-25 18:38:18 +00:00
|
|
|
// Commands that do not require authentication -- skip token refresh for these.
|
|
|
|
|
const NO_AUTH_COMMANDS: &[&str] = &[
|
|
|
|
|
"login",
|
|
|
|
|
"logout",
|
|
|
|
|
"completion",
|
|
|
|
|
"docs",
|
|
|
|
|
"upgrade",
|
|
|
|
|
"telemetry_cmd",
|
|
|
|
|
"check_updates",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let needs_refresh = cli
|
|
|
|
|
.subcommand_name()
|
|
|
|
|
.map(|cmd| !NO_AUTH_COMMANDS.contains(&cmd))
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
if needs_refresh {
|
|
|
|
|
if let Ok(mut configs) = Configs::new() {
|
|
|
|
|
if let Err(e) = client::ensure_valid_token(&mut configs).await {
|
|
|
|
|
eprintln!("{}: {e}", "Warning: failed to refresh OAuth token".yellow());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-30 18:29:22 +00:00
|
|
|
let exec_result = exec_cli(cli).await;
|
2025-03-13 02:09:33 +00:00
|
|
|
|
|
|
|
|
if let Err(e) = exec_result {
|
|
|
|
|
if e.root_cause().to_string() == inquire::InquireError::OperationInterrupted.to_string() {
|
|
|
|
|
return Ok(()); // Exit gracefully if interrupted
|
2023-04-26 13:26:40 +00:00
|
|
|
}
|
2025-11-14 16:04:32 +00:00
|
|
|
|
2025-08-04 20:52:24 +00:00
|
|
|
eprintln!("{e:?}");
|
2025-11-14 16:04:32 +00:00
|
|
|
|
2025-03-13 02:09:33 +00:00
|
|
|
handle_update_task(check_updates_handle).await;
|
|
|
|
|
std::process::exit(1);
|
2023-04-26 13:26:40 +00:00
|
|
|
}
|
2023-03-04 02:44:32 +00:00
|
|
|
|
2025-03-13 02:09:33 +00:00
|
|
|
handle_update_task(check_updates_handle).await;
|
|
|
|
|
|
2023-03-04 02:44:32 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
feat: restructure CLI to object-action pattern (#761)
* feat: restructure CLI to object-action pattern
- Add service subcommands: logs, restart, scale (delegates to existing commands)
- Add project object with list, link, delete subcommands
- Add environment link subcommand
- Restructure variable command with list, set, delete subcommands
- Add VariableDelete GraphQL mutation
- All existing commands continue to work via aliases for backwards compat
* test: add CLI parsing tests for backwards compat and new commands
* refactor: DRY up variable.rs, fix scale arg detection, fix env output order
- Add ServiceContext helper to dedupe project/env/service resolution
- delete_variable now requires service (consistent w/ list/set)
- scale.rs: position-based arg check prevents false triggers
- environment link: output after successful write()
* fix: remove dead code, add service redeploy, fix variable stdin
- service.rs: remove dead code in link_command (duplicate args.service check)
- service.rs: add redeploy subcommand routing
- variable.rs: accept string for set cmd to support --stdin with key-only
- main.rs: add more CLI parsing tests
* fix: service link case-insensitive, multi-var set, spinner cleanup
- service link: restore case-insensitive name/id matching
- variable set: support multiple KEY=val args
- use create_spinner_if helper for json mode
- add tests for multi-var set
* refactor: simplify service link flow
* fix: resolve clippy too_many_arguments warnings
Introduce FetchLogsParams struct to reduce argument count
2026-01-14 18:31:04 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod cli_tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn parse(args: &[&str]) -> Result<clap::ArgMatches, clap::Error> {
|
|
|
|
|
let mut full_args = vec!["railway"];
|
|
|
|
|
full_args.extend(args);
|
|
|
|
|
build_args().try_get_matches_from(full_args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_parses(args: &[&str]) {
|
|
|
|
|
assert!(
|
|
|
|
|
parse(args).is_ok(),
|
|
|
|
|
"Command should parse: railway {}",
|
|
|
|
|
args.join(" ")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_subcommand(args: &[&str], expected: &str) {
|
|
|
|
|
let matches = parse(args).unwrap_or_else(|_| panic!("Failed to parse: {:?}", args));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
matches.subcommand_name(),
|
|
|
|
|
Some(expected),
|
|
|
|
|
"Expected subcommand '{}' for args {:?}",
|
|
|
|
|
expected,
|
|
|
|
|
args
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod backwards_compat {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn root_commands_exist() {
|
|
|
|
|
assert_subcommand(&["logs"], "logs");
|
|
|
|
|
assert_subcommand(&["list"], "list");
|
|
|
|
|
assert_subcommand(&["delete"], "delete");
|
|
|
|
|
assert_subcommand(&["restart"], "restart");
|
|
|
|
|
assert_subcommand(&["scale"], "scale");
|
|
|
|
|
assert_subcommand(&["link"], "link");
|
|
|
|
|
assert_subcommand(&["up"], "up");
|
|
|
|
|
assert_subcommand(&["redeploy"], "redeploy");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_aliases() {
|
|
|
|
|
assert_subcommand(&["variable"], "variable");
|
|
|
|
|
assert_subcommand(&["variables"], "variable");
|
|
|
|
|
assert_subcommand(&["vars"], "variable");
|
|
|
|
|
assert_subcommand(&["var"], "variable");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 19:57:45 +00:00
|
|
|
#[test]
|
|
|
|
|
fn logs_http_flag_parses() {
|
|
|
|
|
assert_parses(&["logs", "--http"]);
|
|
|
|
|
assert_parses(&["logs", "--http", "--lines", "50"]);
|
|
|
|
|
assert_parses(&["service", "logs", "--http"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn logs_http_examples_parse() {
|
|
|
|
|
assert_parses(&["logs", "--http", "--lines", "50"]);
|
|
|
|
|
assert_parses(&[
|
|
|
|
|
"logs",
|
|
|
|
|
"--http",
|
|
|
|
|
"--filter",
|
|
|
|
|
"@path:/api/users @httpStatus:200",
|
|
|
|
|
]);
|
|
|
|
|
assert_parses(&[
|
|
|
|
|
"logs",
|
|
|
|
|
"--http",
|
|
|
|
|
"--json",
|
|
|
|
|
"--filter",
|
|
|
|
|
"@requestId:abcd1234",
|
|
|
|
|
]);
|
|
|
|
|
assert_parses(&[
|
|
|
|
|
"service",
|
|
|
|
|
"logs",
|
|
|
|
|
"--http",
|
|
|
|
|
"--lines",
|
|
|
|
|
"10",
|
|
|
|
|
"--filter",
|
|
|
|
|
"@httpStatus:404",
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
feat: restructure CLI to object-action pattern (#761)
* feat: restructure CLI to object-action pattern
- Add service subcommands: logs, restart, scale (delegates to existing commands)
- Add project object with list, link, delete subcommands
- Add environment link subcommand
- Restructure variable command with list, set, delete subcommands
- Add VariableDelete GraphQL mutation
- All existing commands continue to work via aliases for backwards compat
* test: add CLI parsing tests for backwards compat and new commands
* refactor: DRY up variable.rs, fix scale arg detection, fix env output order
- Add ServiceContext helper to dedupe project/env/service resolution
- delete_variable now requires service (consistent w/ list/set)
- scale.rs: position-based arg check prevents false triggers
- environment link: output after successful write()
* fix: remove dead code, add service redeploy, fix variable stdin
- service.rs: remove dead code in link_command (duplicate args.service check)
- service.rs: add redeploy subcommand routing
- variable.rs: accept string for set cmd to support --stdin with key-only
- main.rs: add more CLI parsing tests
* fix: service link case-insensitive, multi-var set, spinner cleanup
- service link: restore case-insensitive name/id matching
- variable set: support multiple KEY=val args
- use create_spinner_if helper for json mode
- add tests for multi-var set
* refactor: simplify service link flow
* fix: resolve clippy too_many_arguments warnings
Introduce FetchLogsParams struct to reduce argument count
2026-01-14 18:31:04 +00:00
|
|
|
#[test]
|
|
|
|
|
fn variable_legacy_flags() {
|
|
|
|
|
assert_parses(&["variable", "--set", "KEY=value"]);
|
|
|
|
|
assert_parses(&["variable", "--set", "KEY=value", "--set", "KEY2=value2"]);
|
|
|
|
|
assert_parses(&["variable", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["variable", "-e", "production"]);
|
|
|
|
|
assert_parses(&["variable", "--kv"]);
|
|
|
|
|
assert_parses(&["variable", "--json"]);
|
|
|
|
|
assert_parses(&["variable", "--skip-deploys", "--set", "KEY=value"]);
|
|
|
|
|
assert_parses(&["variables", "--set", "KEY=value"]); // via alias
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn environment_implicit_link() {
|
|
|
|
|
assert_parses(&["environment", "production"]); // legacy positional
|
|
|
|
|
assert_parses(&["env", "production"]); // alias
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn service_implicit_link() {
|
|
|
|
|
assert_parses(&["service"]); // prompts for link
|
|
|
|
|
assert_parses(&["service", "myservice"]); // legacy positional link
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn functions_aliases() {
|
|
|
|
|
assert_subcommand(&["functions", "list"], "functions");
|
|
|
|
|
assert_subcommand(&["function", "list"], "functions");
|
|
|
|
|
assert_subcommand(&["func", "list"], "functions");
|
|
|
|
|
assert_subcommand(&["fn", "list"], "functions");
|
|
|
|
|
assert_subcommand(&["funcs", "list"], "functions");
|
|
|
|
|
assert_subcommand(&["fns", "list"], "functions");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn dev_run_aliases() {
|
|
|
|
|
assert_subcommand(&["dev"], "dev");
|
|
|
|
|
assert_subcommand(&["develop"], "dev");
|
|
|
|
|
assert_subcommand(&["run"], "run");
|
|
|
|
|
assert_subcommand(&["local"], "run");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_set_from_stdin_legacy() {
|
|
|
|
|
assert_parses(&["variable", "--set-from-stdin", "MY_KEY"]);
|
|
|
|
|
assert_parses(&["variable", "--set-from-stdin", "KEY", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["variable", "--set-from-stdin", "KEY", "--skip-deploys"]);
|
|
|
|
|
assert_parses(&["variables", "--set-from-stdin", "KEY"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_list_kv_format() {
|
|
|
|
|
assert_parses(&["variable", "--kv"]);
|
|
|
|
|
assert_parses(&["variable", "-k"]);
|
|
|
|
|
assert_parses(&["variables", "--kv"]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mod new_commands {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_subcommands() {
|
|
|
|
|
assert_parses(&["variable", "list"]);
|
|
|
|
|
assert_parses(&["variable", "list", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["variable", "list", "--json"]);
|
|
|
|
|
assert_parses(&["variable", "set", "KEY=value"]);
|
|
|
|
|
assert_parses(&["variable", "set", "KEY=value", "KEY2=value2"]); // multiple
|
|
|
|
|
assert_parses(&["variable", "set", "A=1", "B=2", "C=3", "--skip-deploys"]);
|
|
|
|
|
assert_parses(&["variable", "set", "KEY", "--stdin"]);
|
|
|
|
|
assert_parses(&["variable", "set", "KEY=value", "--skip-deploys"]);
|
|
|
|
|
assert_parses(&["variable", "delete", "KEY"]);
|
|
|
|
|
assert_parses(&["variable", "rm", "KEY"]); // alias
|
|
|
|
|
assert_parses(&["variable", "delete", "KEY", "--json"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn environment_link_subcommand() {
|
|
|
|
|
assert_parses(&["environment", "link"]);
|
|
|
|
|
assert_parses(&["environment", "link", "production"]);
|
|
|
|
|
assert_parses(&["environment", "link", "--json"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn service_subcommands() {
|
|
|
|
|
assert_parses(&["service", "link"]);
|
|
|
|
|
assert_parses(&["service", "status"]);
|
|
|
|
|
assert_parses(&["service", "status", "--all"]);
|
|
|
|
|
assert_parses(&["service", "status", "--json"]);
|
|
|
|
|
assert_parses(&["service", "logs"]);
|
|
|
|
|
assert_parses(&["service", "logs", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["service", "redeploy"]);
|
|
|
|
|
assert_parses(&["service", "redeploy", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["service", "restart"]);
|
|
|
|
|
assert_parses(&["service", "scale"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn project_subcommands() {
|
|
|
|
|
assert_parses(&["project", "list"]);
|
|
|
|
|
assert_parses(&["project", "ls"]); // alias
|
|
|
|
|
assert_parses(&["project", "list", "--json"]);
|
|
|
|
|
assert_parses(&["project", "link"]);
|
|
|
|
|
assert_parses(&["project", "delete"]);
|
|
|
|
|
assert_parses(&["project", "rm"]); // alias
|
|
|
|
|
assert_parses(&["project", "delete", "-y"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_list_aliases() {
|
|
|
|
|
assert_parses(&["variable", "ls"]);
|
|
|
|
|
assert_parses(&["variable", "ls", "--kv"]);
|
|
|
|
|
assert_parses(&["variable", "ls", "-s", "myservice"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_delete_remove_alias() {
|
|
|
|
|
assert_parses(&["variable", "remove", "KEY"]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_set_stdin_key_only() {
|
|
|
|
|
assert_parses(&["variable", "set", "KEY", "--stdin"]);
|
|
|
|
|
assert_parses(&["variable", "set", "MY_VAR", "--stdin", "-s", "myservice"]);
|
|
|
|
|
assert_parses(&["variable", "set", "SECRET", "--stdin", "--skip-deploys"]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|