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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/live-gh-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Live GH Smoke

on:
workflow_dispatch:

jobs:
live-gh-smoke:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v4

- name: Set up Dart
uses: dart-lang/setup-dart@v1

- name: Install dependencies
run: dart pub get

- name: Run live GitHub smoke tests
env:
DRX_ENABLE_LIVE_GH_TESTS: '1'
run: dart test test/gh_live_smoke_test.dart -r expanded
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Unreleased

- Add `--gh-mode` (`binary|source|auto`) and `--git-path` to support running Dart CLIs directly from GitHub source.
- Add source-mode execution for Dart GitHub repos via sandboxed git dependencies and `auto` fallback when release binaries are unavailable.
- Default `gh:` execution to `--gh-mode auto` (binary first, source fallback).
- Add AOT support for GitHub Dart source mode, with `--runtime auto` trying AOT first and falling back to JIT.

## 0.3.1

- Add installer one-liner smoke tests across Linux/macOS/Windows on `x64` and `arm64`.
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Explicit source selection:
```bash
drx --from pub:<package[@version]> <command> [--] [args...]
drx --from gh:<owner>/<repo[@tag]> <command> [--] [args...]
# Optional for gh source: --gh-mode binary|source|auto --git-path <path>
```

Examples:
Expand All @@ -65,6 +66,12 @@ drx --from gh:BurntSushi/ripgrep rg -- --version
drx --from gh:junegunn/fzf fzf -- --version
drx --from gh:charmbracelet/gum gum -- --version

# Run a Dart CLI directly from GitHub source:
drx --gh-mode source --from gh:leehack/mcp_dart@mcp_dart_cli-v0.1.6 --git-path packages/mcp_dart_cli mcp_dart_cli:mcp_dart -- --help

# Try release binary first, then fallback to source mode:
drx --gh-mode auto --from gh:leehack/mcp_dart@mcp_dart_cli-v0.1.6 --git-path packages/mcp_dart_cli mcp_dart -- --help

# Some repos do not publish checksums:
drx --allow-unsigned --from gh:sharkdp/fd fd -- --version
```
Expand All @@ -83,17 +90,27 @@ drx --json versions gh:cli/cli
drx --json cache list
```

## Runtime Modes (pub source)
## Runtime Modes (Dart source execution)

- `--runtime auto` (default): prefer AOT, fallback to JIT
- `--runtime auto` (default): try AOT first, fallback to JIT
- `--runtime jit`: `dart run`
- `--runtime aot`: compile and run native executable

These runtime modes apply to `pub:` and `gh:` when execution is from Dart source
(`--gh-mode source`, or `--gh-mode auto` when it falls back to source mode).

## Security Defaults

- GitHub assets require checksum verification by default.
- Unsigned assets are blocked unless you pass `--allow-unsigned`.

## GitHub Mode Behavior

- `--gh-mode auto` (default for `gh:`): try precompiled release assets first, then fallback to source mode when no compatible binary is available.
- `--gh-mode binary`: run precompiled release assets only.
- `--gh-mode source`: run Dart CLI from GitHub source via sandboxed `dart pub get` and runtime selection (`jit`/`aot`/`auto`).
- `--git-path <path>` selects a package path inside a monorepo for source mode.

## Platform Support

- Linux: `x64`, `arm64`
Expand Down
21 changes: 21 additions & 0 deletions doc/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ This source is language-agnostic. Your executable can be built from any stack
(for example Go, Rust, C/C++, Zig, or .NET native AOT) as long as release
assets match platform/arch naming and include checksums.

### Dart source fallback mode

For Dart tools that do not publish release binaries, `drx` can run directly from
GitHub source:

```bash
drx --gh-mode source --from gh:<owner>/<repo[@tag]> <executable-or-package:executable> -- [args...]
```

Use `--git-path <path>` when the Dart package is in a monorepo subdirectory.

```bash
drx --gh-mode source --from gh:leehack/mcp_dart@mcp_dart_cli-v0.1.6 --git-path packages/mcp_dart_cli mcp_dart_cli:mcp_dart -- --help
```

`--gh-mode auto` (default for `gh:`) tries binary release assets first and
falls back to source mode when no compatible binary is available.

When running from source mode, `--runtime auto` tries AOT first and falls back
to JIT. You can also force `--runtime aot` or `--runtime jit`.

### Requirements

1. Publish binaries on GitHub Releases.
Expand Down
13 changes: 13 additions & 0 deletions doc/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ dart run tool/check_coverage.dart 80 coverage/lcov.info

- `/.github/workflows/ci.yml`
- analyze + tests on Linux/macOS/Windows (`x64`, `arm64`) and coverage gate.
- includes GH mode/runtime integration matrix coverage (`test/gh_integration_test.dart`).
- `/.github/workflows/installer-smoke.yml`
- validates one-liner install scripts on Linux/macOS/Windows (`x64`, `arm64`).
- `/.github/workflows/live-gh-smoke.yml`
- manual live-network smoke tests against real GitHub repos.
- `/.github/workflows/publish-pubdev.yml`
- publishes to pub.dev when a tag like `vX.Y.Z` is pushed.
- `/.github/workflows/release-binaries.yml`
Expand Down Expand Up @@ -74,3 +77,13 @@ without changing the package version.
- Installer scripts support `DRX_DOWNLOAD_BASE` override for local/test payloads.
- This is used by installer smoke workflow to test the one-liner flow end-to-end
without depending on external GitHub release artifacts.

## Live GitHub Smoke Tests

- `test/gh_live_smoke_test.dart` is disabled by default and only runs when
`DRX_ENABLE_LIVE_GH_TESTS=1`.
- Run locally:

```bash
DRX_ENABLE_LIVE_GH_TESTS=1 dart test test/gh_live_smoke_test.dart
```
4 changes: 4 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ drx --from gh:BurntSushi/ripgrep rg -- --version
drx --from gh:junegunn/fzf fzf -- --version
drx --from gh:charmbracelet/gum gum -- --version
drx --allow-unsigned --from gh:sharkdp/fd fd -- --version

# GitHub Dart source mode
drx --gh-mode source --from gh:leehack/mcp_dart@mcp_dart_cli-v0.1.6 --git-path packages/mcp_dart_cli mcp_dart_cli:mcp_dart -- --help
drx --gh-mode auto --from gh:leehack/mcp_dart@mcp_dart_cli-v0.1.6 --git-path packages/mcp_dart_cli mcp_dart -- --help
```

Use runtime controls for pub tools:
Expand Down
20 changes: 20 additions & 0 deletions lib/src/cache_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ final class DrxPaths {
return Directory(p.join(cacheDirectory.path, 'gh', key));
}

/// Cache location for GitHub source-mode Dart installs.
Directory ghSourceDir(
String owner,
String repo,
String ref,
String gitPath,
HostPlatform platform,
) {
final key = stableKey([
'gh-source',
owner,
repo,
ref,
gitPath,
platform.os,
platform.arch,
]);
return Directory(p.join(cacheDirectory.path, 'gh-source', key));
}

/// Lock file path derived from a deterministic key.
File lockFileFor(String key) {
final hash = stableKey([key]);
Expand Down
74 changes: 74 additions & 0 deletions lib/src/cli_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ final class CliParser {
RuntimeMode runtime = RuntimeMode.auto;
var refresh = false;
var isolated = false;
GhMode ghMode = GhMode.auto;
var ghModeExplicit = false;
String? gitPath;
String? asset;
var allowUnsigned = false;
var verbose = false;
Expand Down Expand Up @@ -60,6 +63,36 @@ final class CliParser {
continue;
}

if (token.startsWith('--gh-mode=')) {
ghMode = _parseGhMode(token.substring('--gh-mode='.length).trim());
ghModeExplicit = true;
i++;
continue;
}
if (token == '--gh-mode') {
if (i + 1 >= argv.length) {
throw const CliParseException('Missing value for --gh-mode.');
}
ghMode = _parseGhMode(argv[i + 1].trim());
ghModeExplicit = true;
i += 2;
continue;
}

if (token.startsWith('--git-path=')) {
gitPath = token.substring('--git-path='.length).trim();
i++;
continue;
}
if (token == '--git-path') {
if (i + 1 >= argv.length) {
throw const CliParseException('Missing value for --git-path.');
}
gitPath = argv[i + 1].trim();
i += 2;
continue;
}

if (token.startsWith('--from=')) {
fromRaw = token.substring('--from='.length).trim();
i++;
Expand Down Expand Up @@ -149,6 +182,30 @@ final class CliParser {
}

final commandArgs = _extractCommandArgs(commandArgsInput);
final normalizedGitPath = gitPath?.trim();

if (source.type != SourceType.gh && ghModeExplicit) {
throw const CliParseException('--gh-mode is only valid for gh source.');
}
if (source.type != SourceType.gh && normalizedGitPath != null) {
throw const CliParseException('--git-path is only valid for gh source.');
}

if (source.type != SourceType.gh) {
ghMode = GhMode.binary;
}

if (source.type == SourceType.gh &&
ghMode == GhMode.binary &&
normalizedGitPath != null &&
normalizedGitPath.isNotEmpty) {
throw const CliParseException(
'--git-path requires --gh-mode source or --gh-mode auto.',
);
}
if (normalizedGitPath != null && normalizedGitPath.isEmpty) {
throw const CliParseException('--git-path cannot be empty.');
}

final request = CommandRequest(
source: source,
Expand All @@ -159,6 +216,8 @@ final class CliParser {
isolated: isolated,
allowUnsigned: allowUnsigned,
verbose: verbose,
ghMode: ghMode,
gitPath: normalizedGitPath,
asset: asset?.isEmpty == true ? null : asset,
);

Expand All @@ -180,6 +239,21 @@ final class CliParser {
}
}

GhMode _parseGhMode(String value) {
switch (value) {
case 'binary':
return GhMode.binary;
case 'source':
return GhMode.source;
case 'auto':
return GhMode.auto;
default:
throw CliParseException(
'Invalid gh mode "$value". Use binary, source, or auto.',
);
}
}

({SourceSpec source, String command}) _parseDefaultPubTarget(String token) {
final at = token.lastIndexOf('@');
final hasVersion = at > 0;
Expand Down
7 changes: 5 additions & 2 deletions lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ Usage:
drx <package:executable[@version]> [--] [args...]
drx --from pub:<package[@version]> <command> [--] [args...]
drx --from gh:<owner>/<repo[@tag]> <command> [--] [args...]
[--gh-mode binary|source|auto] [--git-path <path>]
drx cache [list|clean|prune] [--json]
drx cache prune [--max-age-days N] [--max-size-mb N] [--json]
drx versions <package|pub:pkg|gh:owner/repo> [--limit N] [--json]
Expand All @@ -465,10 +466,12 @@ Options:
-h, --help Show this help.
--version Show version.
--from <source> Source: pub:... or gh:...
--runtime <mode> auto | jit | aot
--runtime <mode> auto | jit | aot (for pub and gh source modes)
--refresh Refresh cached artifacts.
--isolated Use isolated temporary environment.
--asset <name> Asset override for gh source.
--gh-mode <mode> binary | source | auto (gh source only; default: auto)
--git-path <path> Package path in GitHub monorepo (gh source mode).
--asset <name> Asset override for gh binary mode.
--allow-unsigned Allow running unsigned gh assets.
--json JSON output (cache/versions commands).
-v, --verbose Verbose output.
Expand Down
Loading