Imported from MindscapeHQ/raygun-cli (
AGENTS.md). Install upstream withnpx skills add MindscapeHQ/raygun-cli. Copyright stays with the author.
Raygun CLI - Agent Guide
Build/Test Commands
dart pub get --enforce-lockfile- Install dependencies from the committed lockfiledart test- Run all testsdart test test/config_props_test.dart- Run single test filedart analyze- Run linter/analysis (uses package:lints/recommended.yaml)dart compile exe bin/raygun_cli.dart -o raygun-cli- Build executabledart run bin/raygun_cli.dart- Run CLI locallydart format .- Format codedart run build_runner build- Generate mocks (one-time)dart run build_runner watch- Auto-regenerate mocks on changes
Architecture
- CLI Tool: Uploads sourcemaps, manages obfuscation symbols, tracks deployments for Raygun.com
- Main Entry:
bin/raygun_cli.dart- CLI argument parsing and command routing - Commands:
lib/src/- Five main command modules: sourcemap, symbols, deployments, proguard, dsym - APIs: Each command has corresponding API client (
*_api.dart) for Raygun REST API calls - Config:
config_props.darthandles arg parsing with env var and.envfile fallbacks (RAYGUN_APP_ID, RAYGUN_TOKEN, RAYGUN_API_KEY). Resolution order: CLI arg > env var >.envfile. Empty and whitespace-only values at any tier are treated as missing and fall through. With-v/--verbose, each property's resolved source is logged. Seelib/src/config_file.dartfor.envdiscovery logic — discovery walks up from CWD, stopping at$HOME/%USERPROFILE%; if neither is set, discovery is restricted to CWD only.
Directory Structure
lib/src/[command]/
├── [command]_command.dart # CLI parsing and routing
├── [command]_api.dart # Raygun API integration
└── [command].dart # Business logic (if complex)
lib/src/core/ # Shared utilities (RaygunCommand, RaygunApi builders)
test/[command]/ # Tests mirror lib structure
Dependency Flow
bin/raygun_cli.dart → lib/src/[command]/[command]_command.dart → *_api.dart
- Commands are stateless; API clients handle HTTP
- Commands receive API clients via constructor (dependency injection)
Code Style
- Imports: Standard library first, then package imports, then relative imports
- Naming: Snake_case for files/dirs, camelCase for variables, PascalCase for classes
- Functions: Use
Future<bool>for async operations, return success/failure status - Errors: Print error messages to console, return false on failure
- Types: Use explicit types for public APIs, required named parameters preferred
- Strings: Use single quotes, string interpolation with $variable or ${expression}
- Comments: Use /// for public API documentation, avoid inline comments
Error Handling & Exit Codes
Standard error handling patterns used throughout:
- Exit code 0: Success
- Exit code 1: Failure (operation didn't succeed)
- Exit code 2: Error (exception or invalid input)
- Print errors to console before returning/exiting
- Use
Future<bool>return type for async operations - Chain with
.then()and.catchError()for error handling
Example:
run().then((result) {
if (result) {
exit(0);
} else {
exit(2);
}
}).catchError((e) {
print('Error: $e');
exit(2);
});
Command Implementation Patterns
All commands extend the RaygunCommand abstract class:
Required Implementation
class MyCommand extends RaygunCommand {
const MyCommand({required this.api});
final MyApi api;
@override
String get name => 'mycommand';
@override
ArgParser buildParser() { /* ... */ }
@override
void execute(ArgResults command, bool verbose) { /* ... */ }
}
Command Patterns
- Help Flag: Always check for
--helpflag first and exit(0) - Config Loading: Use
ConfigProp.load()for app-id, token, api-key (exits if missing) - Subcommands: Use
ArgParser.addCommand()(see symbols: upload/list/delete) - Verbose Flag: Available in all commands (inherited from main parser)
- Mandatory Args: Use
mandatory: truein ArgParser for required flags
Example: Subcommands Pattern (symbols command)
ArgParser buildParser() {
return ArgParser()
..addOption('app-id')
..addOption('token')
..addCommand('upload')
..addCommand('list')
..addCommand('delete');
}
API Client Patterns
Each command has a corresponding API client:
Builder Pattern for Requests
RaygunMultipartRequestBuilder- For file uploadsRaygunPostRequestBuilder- For JSON POST requests
Example:
final request = RaygunMultipartRequestBuilder(url, 'POST')
.addBearerToken(token)
.addFile('file', filePath)
.addField('version', version)
.build();
API Client Structure
- Factory method pattern: Use static
.create()for production instances - Return
Future<bool>to indicate success/failure - Print response codes and messages for debugging
- Handle HTTP responses and errors appropriately
Testing Patterns & Mock Generation
Test Structure
- Tests use
mockitofor mocking API clients - Test files mirror
lib/structure (e.g.,lib/src/symbols/→test/symbols/) - Mock files use
.mocks.dartsuffix and are git-tracked - Generate mocks with:
dart run build_runner build
Test Pattern
// 1. Generate mock classes with @GenerateMocks annotation
@GenerateMocks([MyApi])
void main() {
group('MyCommand', () {
late MockMyApi mockApi;
setUp(() {
mockApi = MockMyApi();
});
test('description', () async {
// 2. Setup mock behavior
when(mockApi.someMethod()).thenAnswer((_) async => true);
// 3. Inject mock into command
final command = MyCommand(api: mockApi);
// 4. Execute and verify
final result = await command.run(...);
expect(result, true);
});
});
}
Mock Regeneration
- After changing API signatures, run:
dart run build_runner build - Use
watchmode during development:dart run build_runner watch
Dependency Injection
Pattern
- Commands receive API clients via constructor
- Global command instances use
.create()factories - Tests inject mock API clients
- Enables testing without hitting real APIs
Example:
// Production usage (in command file)
SymbolsCommand symbolsCommand = SymbolsCommand(api: SymbolsApi.create());
// Test usage
final mockApi = MockSymbolsApi();
final command = SymbolsCommand(api: mockApi);
CI/CD & Release Workflow
PR Requirements
- Title: Must follow Conventional Commits (enforced by
.github/workflows/pr.yml) - Checks: All must pass - enforced lockfile install, format, analyze, test
- Platforms: Multi-platform builds run automatically (Linux, macOS, Windows)
Lockfile and Supply-Chain Policy
pubspec.lockis intentionally committed because this repository builds a CLI executable and release binaries.- Use
dart pub get --enforce-lockfilein CI, release workflows, and source-build verification. This fails when the lockfile is missing, out of sync withpubspec.yaml, or package content hashes do not match. - Dependency updates should be isolated to dependency-specific PRs, usually from Dependabot. Review
pubspec.lockchanges alongside anypubspec.yamlchanges. - Do not broad-upgrade unrelated dependencies in feature or fix PRs unless needed for the task.
- CI build artifacts include the compiled binary,
pubspec.lock, andSHA256SUMS. - Release archives include the platform binary,
pubspec.lock,build-manifest.txt, andSHA256SUMSso users can verify and reproduce builds from the tagged commit.
Conventional Commits Format
Examples:
feat: add new command for Xfix: resolve issue with Ychore: update dependenciesdocs: update README
Version Management
IMPORTANT: Update BOTH files when releasing:
pubspec.yaml- version fieldbin/raygun_cli.dart- version constant
Workflows
- pr.yml: Validates PR title format
- main.yml: Enforces the lockfile, runs tests, format check, analysis, builds binaries for all platforms, and uploads binaries with lockfile/checksums
- release.yml: On GitHub release, enforces the lockfile, builds zipped binaries, and includes lockfile/checksum/build-manifest files in each archive
Development Workflow
Local Testing
# Run CLI locally with arguments
dart run bin/raygun_cli.dart <command> <args>
# Use verbose flag for debug output
dart run bin/raygun_cli.dart -v sourcemap --help
# Set environment variables for testing
export RAYGUN_APP_ID=test-app-id
export RAYGUN_TOKEN=test-token
export RAYGUN_API_KEY=test-api-key
Testing Workflow
- Write/modify API client code
- Add
@GenerateMocks([MyApi])to test file - Run
dart run build_runner buildto generate mocks - Write tests using mock instances
- Run
dart testto verify
Build for Distribution
# Compile for current platform
dart pub get --enforce-lockfile
dart compile exe bin/raygun_cli.dart -o raygun-cli
# Note: Cross-compilation not supported; use CI for other platforms
Common Gotchas & Best Practices
Validation Order
- Always validate
--helpflag first before parsing other args ConfigProp.load()callsexit(2)if required config is missing- Check mandatory args before executing business logic
File Operations
- Use
File.existsSync()before file operations - Throw descriptive exceptions if files don't exist
- Use
.split("/").lastto get filename from path
Argument Parsing
- Use
command.wasParsed('flag')to check if flag was provided - Use
command['option']to get option value - Use
command.command?.nameto get subcommand name
Async Patterns
- Prefer
.then()and.catchError()over try/catch for CLI commands - Always handle errors gracefully with user-friendly messages
- Use
Future<bool>for operations that can succeed or fail
String Conventions
- Always use single quotes for strings (Dart convention)
- Use string interpolation:
'Value: $variable'or'Value: ${expression}'
Quick Reference
Command Examples
# Sourcemap upload (Flutter)
dart run bin/raygun_cli.dart sourcemap -p flutter \
--uri=https://example.com/main.dart.js \
--app-id=XXX --token=YYY
# Sourcemap upload (single file)
dart run bin/raygun_cli.dart sourcemap \
--input-map=path/to/index.js.map \
--uri=https://example.com/index.js \
--app-id=XXX --token=YYY
# Symbols upload
dart run bin/raygun_cli.dart symbols upload \
--path=app.android-arm64.symbols \
--version=1.0.0 \
--app-id=XXX --token=YYY
# Symbols list
dart run bin/raygun_cli.dart symbols list \
--app-id=XXX --token=YYY
# Symbols delete
dart run bin/raygun_cli.dart symbols delete \
--id=2c7a3u3 \
--app-id=XXX --token=YYY
# Deployments tracking
dart run bin/raygun_cli.dart deployments \
--version=1.0.0 \
--token=YYY \
--api-key=ZZZ \
--scm-type=GitHub \
--scm-identifier=abc123
# Proguard upload
dart run bin/raygun_cli.dart proguard \
--app-id=XXX \
--version=1.0.0 \
--path=mapping.txt \
--external-access-token=EAT \
--overwrite
# iOS dSYM upload
dart run bin/raygun_cli.dart dsym \
--app-id=XXX \
--path=path/to/dsym.zip \
--external-access-token=EAT
Environment Variables
export RAYGUN_APP_ID=your-app-id
export RAYGUN_TOKEN=your-token
export RAYGUN_API_KEY=your-api-key
.env Config File
The same keys are also read from a .env file in the current working directory
(or any parent directory, walking up to $HOME/%USERPROFILE%). When neither
HOME nor USERPROFILE is set (sandboxed CI runners, minimal containers),
discovery is restricted to CWD only — no upward walking. An explicit path can
always be passed with --config-file=<path>.
# .env
RAYGUN_APP_ID=your-app-id
RAYGUN_TOKEN=your-token
RAYGUN_API_KEY=your-api-key
Resolution precedence: CLI argument > environment variable > .env file.
Empty (KEY=) and whitespace-only (KEY=" ") values at any tier are treated
as missing and fall through to the next source — this prevents opaque HTTP
4xx errors when a user has copied the example .env and forgotten to fill
in a value. Use -v/--verbose to print which source supplied each value.
A sample is committed at example/.env.example.
Known TODOs & Future Improvements
- NodeJS sourcemap platform support (currently stubbed in sourcemap command)
- System package manager installations (brew, apt, etc.)
Troubleshooting
Mock Generation Issues
- Ensure
@GenerateMocksannotation is present in test file - Run
dart pub get --enforce-lockfileto ensure dependencies are installed from the committed lockfile - Clean and rebuild:
dart run build_runner clean && dart run build_runner build
Build Issues
- Ensure Dart SDK version matches
pubspec.yamlrequirement (^3.11.0) - Run
dart pub get --enforce-lockfileto verify the committed lockfile is usable - For intentional dependency updates, update
pubspec.yamlif needed, rundart pub upgrade <package>, and commit the resultingpubspec.lock - Check that version in
bin/raygun_cli.dartmatchespubspec.yaml
Test Failures
- Verify mocks are regenerated after API changes
- Check that mock behavior is properly stubbed with
when() - Ensure async tests use
async/awaitor return Future