@hasna/terminal 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +322 -0
- package/dist/cache.js +41 -0
- package/dist/cli.js +64 -16
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +107 -0
- package/dist/context-hints.js +275 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +123 -0
- package/dist/expand-store.js +38 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +569 -0
- package/dist/noise-filter.js +86 -0
- package/dist/output-processor.js +129 -0
- package/dist/output-router.js +41 -0
- package/dist/output-store.js +111 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +99 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/groq.js +95 -0
- package/dist/providers/index.js +73 -0
- package/dist/providers/xai.js +95 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +136 -0
- package/dist/search/content-search.js +68 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +320 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +173 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tool-profiles.js +122 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +8 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- package/CONTRIBUTING.md +0 -80
- package/benchmarks/benchmark.mjs +0 -115
- package/imported_modules.txt +0 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
- package/tsconfig.json +0 -15
package/temp/rtk/src/gh_cmd.rs
DELETED
|
@@ -1,1651 +0,0 @@
|
|
|
1
|
-
//! GitHub CLI (gh) command output compression.
|
|
2
|
-
//!
|
|
3
|
-
//! Provides token-optimized alternatives to verbose `gh` commands.
|
|
4
|
-
//! Focuses on extracting essential information from JSON outputs.
|
|
5
|
-
|
|
6
|
-
use crate::git;
|
|
7
|
-
use crate::tracking;
|
|
8
|
-
use crate::utils::{ok_confirmation, truncate};
|
|
9
|
-
use anyhow::{Context, Result};
|
|
10
|
-
use lazy_static::lazy_static;
|
|
11
|
-
use regex::Regex;
|
|
12
|
-
use serde_json::Value;
|
|
13
|
-
use std::process::Command;
|
|
14
|
-
|
|
15
|
-
lazy_static! {
|
|
16
|
-
static ref HTML_COMMENT_RE: Regex = Regex::new(r"(?s)<!--.*?-->").unwrap();
|
|
17
|
-
static ref BADGE_LINE_RE: Regex =
|
|
18
|
-
Regex::new(r"(?m)^\s*\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)\s*$").unwrap();
|
|
19
|
-
static ref IMAGE_ONLY_LINE_RE: Regex = Regex::new(r"(?m)^\s*!\[[^\]]*\]\([^)]*\)\s*$").unwrap();
|
|
20
|
-
static ref HORIZONTAL_RULE_RE: Regex =
|
|
21
|
-
Regex::new(r"(?m)^\s*(?:---+|\*\*\*+|___+)\s*$").unwrap();
|
|
22
|
-
static ref MULTI_BLANK_RE: Regex = Regex::new(r"\n{3,}").unwrap();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// Filter markdown body to remove noise while preserving meaningful content.
|
|
26
|
-
/// Removes HTML comments, badge lines, image-only lines, horizontal rules,
|
|
27
|
-
/// and collapses excessive blank lines. Preserves code blocks untouched.
|
|
28
|
-
fn filter_markdown_body(body: &str) -> String {
|
|
29
|
-
if body.is_empty() {
|
|
30
|
-
return String::new();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Split into code blocks and non-code segments
|
|
34
|
-
let mut result = String::new();
|
|
35
|
-
let mut remaining = body;
|
|
36
|
-
|
|
37
|
-
loop {
|
|
38
|
-
// Find next code block opening (``` or ~~~)
|
|
39
|
-
let fence_pos = remaining
|
|
40
|
-
.find("```")
|
|
41
|
-
.or_else(|| remaining.find("~~~"))
|
|
42
|
-
.map(|pos| {
|
|
43
|
-
let fence = if remaining[pos..].starts_with("```") {
|
|
44
|
-
"```"
|
|
45
|
-
} else {
|
|
46
|
-
"~~~"
|
|
47
|
-
};
|
|
48
|
-
(pos, fence)
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
match fence_pos {
|
|
52
|
-
Some((start, fence)) => {
|
|
53
|
-
// Filter the text before the code block
|
|
54
|
-
let before = &remaining[..start];
|
|
55
|
-
result.push_str(&filter_markdown_segment(before));
|
|
56
|
-
|
|
57
|
-
// Find the closing fence
|
|
58
|
-
let after_open = start + fence.len();
|
|
59
|
-
// Skip past the opening fence line
|
|
60
|
-
let code_start = remaining[after_open..]
|
|
61
|
-
.find('\n')
|
|
62
|
-
.map(|p| after_open + p + 1)
|
|
63
|
-
.unwrap_or(remaining.len());
|
|
64
|
-
|
|
65
|
-
let close_pos = remaining[code_start..]
|
|
66
|
-
.find(fence)
|
|
67
|
-
.map(|p| code_start + p + fence.len());
|
|
68
|
-
|
|
69
|
-
match close_pos {
|
|
70
|
-
Some(end) => {
|
|
71
|
-
// Preserve the entire code block as-is
|
|
72
|
-
result.push_str(&remaining[start..end]);
|
|
73
|
-
// Include the rest of the closing fence line
|
|
74
|
-
let after_close = remaining[end..]
|
|
75
|
-
.find('\n')
|
|
76
|
-
.map(|p| end + p + 1)
|
|
77
|
-
.unwrap_or(remaining.len());
|
|
78
|
-
result.push_str(&remaining[end..after_close]);
|
|
79
|
-
remaining = &remaining[after_close..];
|
|
80
|
-
}
|
|
81
|
-
None => {
|
|
82
|
-
// Unclosed code block — preserve everything
|
|
83
|
-
result.push_str(&remaining[start..]);
|
|
84
|
-
remaining = "";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
None => {
|
|
89
|
-
// No more code blocks, filter the rest
|
|
90
|
-
result.push_str(&filter_markdown_segment(remaining));
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Final cleanup: trim trailing whitespace
|
|
97
|
-
result.trim().to_string()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// Filter a markdown segment that is NOT inside a code block.
|
|
101
|
-
fn filter_markdown_segment(text: &str) -> String {
|
|
102
|
-
let mut s = HTML_COMMENT_RE.replace_all(text, "").to_string();
|
|
103
|
-
s = BADGE_LINE_RE.replace_all(&s, "").to_string();
|
|
104
|
-
s = IMAGE_ONLY_LINE_RE.replace_all(&s, "").to_string();
|
|
105
|
-
s = HORIZONTAL_RULE_RE.replace_all(&s, "").to_string();
|
|
106
|
-
s = MULTI_BLANK_RE.replace_all(&s, "\n\n").to_string();
|
|
107
|
-
s
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/// Check if args contain --json flag (user wants specific JSON fields, not RTK filtering)
|
|
111
|
-
fn has_json_flag(args: &[String]) -> bool {
|
|
112
|
-
args.iter().any(|a| a == "--json")
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/// Extract a positional identifier (PR/issue number) from args, returning it
|
|
116
|
-
/// separately from the remaining extra flags (like -R, --repo, etc.).
|
|
117
|
-
/// Handles both `view 123 -R owner/repo` and `view -R owner/repo 123`.
|
|
118
|
-
fn extract_identifier_and_extra_args(args: &[String]) -> Option<(String, Vec<String>)> {
|
|
119
|
-
if args.is_empty() {
|
|
120
|
-
return None;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Known gh flags that take a value — skip these and their values
|
|
124
|
-
let flags_with_value = [
|
|
125
|
-
"-R",
|
|
126
|
-
"--repo",
|
|
127
|
-
"-q",
|
|
128
|
-
"--jq",
|
|
129
|
-
"-t",
|
|
130
|
-
"--template",
|
|
131
|
-
"--job",
|
|
132
|
-
"--attempt",
|
|
133
|
-
];
|
|
134
|
-
let mut identifier = None;
|
|
135
|
-
let mut extra = Vec::new();
|
|
136
|
-
let mut skip_next = false;
|
|
137
|
-
|
|
138
|
-
for arg in args {
|
|
139
|
-
if skip_next {
|
|
140
|
-
extra.push(arg.clone());
|
|
141
|
-
skip_next = false;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if flags_with_value.contains(&arg.as_str()) {
|
|
145
|
-
extra.push(arg.clone());
|
|
146
|
-
skip_next = true;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if arg.starts_with('-') {
|
|
150
|
-
extra.push(arg.clone());
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
// First non-flag arg is the identifier (number/URL)
|
|
154
|
-
if identifier.is_none() {
|
|
155
|
-
identifier = Some(arg.clone());
|
|
156
|
-
} else {
|
|
157
|
-
extra.push(arg.clone());
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
identifier.map(|id| (id, extra))
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/// Run a gh command with token-optimized output
|
|
165
|
-
pub fn run(subcommand: &str, args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
166
|
-
// When user explicitly passes --json, they want raw gh JSON output, not RTK filtering
|
|
167
|
-
if has_json_flag(args) {
|
|
168
|
-
return run_passthrough("gh", subcommand, args);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
match subcommand {
|
|
172
|
-
"pr" => run_pr(args, verbose, ultra_compact),
|
|
173
|
-
"issue" => run_issue(args, verbose, ultra_compact),
|
|
174
|
-
"run" => run_workflow(args, verbose, ultra_compact),
|
|
175
|
-
"repo" => run_repo(args, verbose, ultra_compact),
|
|
176
|
-
"api" => run_api(args, verbose),
|
|
177
|
-
_ => {
|
|
178
|
-
// Unknown subcommand, pass through
|
|
179
|
-
run_passthrough("gh", subcommand, args)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
fn run_pr(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
185
|
-
if args.is_empty() {
|
|
186
|
-
return run_passthrough("gh", "pr", args);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
match args[0].as_str() {
|
|
190
|
-
"list" => list_prs(&args[1..], verbose, ultra_compact),
|
|
191
|
-
"view" => view_pr(&args[1..], verbose, ultra_compact),
|
|
192
|
-
"checks" => pr_checks(&args[1..], verbose, ultra_compact),
|
|
193
|
-
"status" => pr_status(verbose, ultra_compact),
|
|
194
|
-
"create" => pr_create(&args[1..], verbose),
|
|
195
|
-
"merge" => pr_merge(&args[1..], verbose),
|
|
196
|
-
"diff" => pr_diff(&args[1..], verbose),
|
|
197
|
-
"comment" => pr_action("commented", &args, verbose),
|
|
198
|
-
"edit" => pr_action("edited", &args, verbose),
|
|
199
|
-
_ => run_passthrough("gh", "pr", args),
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
fn list_prs(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
204
|
-
let timer = tracking::TimedExecution::start();
|
|
205
|
-
|
|
206
|
-
let mut cmd = Command::new("gh");
|
|
207
|
-
cmd.args([
|
|
208
|
-
"pr",
|
|
209
|
-
"list",
|
|
210
|
-
"--json",
|
|
211
|
-
"number,title,state,author,updatedAt",
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
// Pass through additional flags
|
|
215
|
-
for arg in args {
|
|
216
|
-
cmd.arg(arg);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
let output = cmd.output().context("Failed to run gh pr list")?;
|
|
220
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
221
|
-
|
|
222
|
-
if !output.status.success() {
|
|
223
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
224
|
-
timer.track("gh pr list", "rtk gh pr list", &stderr, &stderr);
|
|
225
|
-
eprintln!("{}", stderr.trim());
|
|
226
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let json: Value =
|
|
230
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh pr list output")?;
|
|
231
|
-
|
|
232
|
-
let mut filtered = String::new();
|
|
233
|
-
|
|
234
|
-
if let Some(prs) = json.as_array() {
|
|
235
|
-
if ultra_compact {
|
|
236
|
-
filtered.push_str("PRs\n");
|
|
237
|
-
println!("PRs");
|
|
238
|
-
} else {
|
|
239
|
-
filtered.push_str("📋 Pull Requests\n");
|
|
240
|
-
println!("📋 Pull Requests");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for pr in prs.iter().take(20) {
|
|
244
|
-
let number = pr["number"].as_i64().unwrap_or(0);
|
|
245
|
-
let title = pr["title"].as_str().unwrap_or("???");
|
|
246
|
-
let state = pr["state"].as_str().unwrap_or("???");
|
|
247
|
-
let author = pr["author"]["login"].as_str().unwrap_or("???");
|
|
248
|
-
|
|
249
|
-
let state_icon = if ultra_compact {
|
|
250
|
-
match state {
|
|
251
|
-
"OPEN" => "O",
|
|
252
|
-
"MERGED" => "M",
|
|
253
|
-
"CLOSED" => "C",
|
|
254
|
-
_ => "?",
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
match state {
|
|
258
|
-
"OPEN" => "🟢",
|
|
259
|
-
"MERGED" => "🟣",
|
|
260
|
-
"CLOSED" => "🔴",
|
|
261
|
-
_ => "⚪",
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
let line = format!(
|
|
266
|
-
" {} #{} {} ({})\n",
|
|
267
|
-
state_icon,
|
|
268
|
-
number,
|
|
269
|
-
truncate(title, 60),
|
|
270
|
-
author
|
|
271
|
-
);
|
|
272
|
-
filtered.push_str(&line);
|
|
273
|
-
print!("{}", line);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if prs.len() > 20 {
|
|
277
|
-
let more_line = format!(" ... {} more (use gh pr list for all)\n", prs.len() - 20);
|
|
278
|
-
filtered.push_str(&more_line);
|
|
279
|
-
print!("{}", more_line);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
timer.track("gh pr list", "rtk gh pr list", &raw, &filtered);
|
|
284
|
-
Ok(())
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
fn should_passthrough_pr_view(extra_args: &[String]) -> bool {
|
|
288
|
-
extra_args
|
|
289
|
-
.iter()
|
|
290
|
-
.any(|a| a == "--json" || a == "--jq" || a == "--web")
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
fn view_pr(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
294
|
-
let timer = tracking::TimedExecution::start();
|
|
295
|
-
|
|
296
|
-
let (pr_number, extra_args) = match extract_identifier_and_extra_args(args) {
|
|
297
|
-
Some(result) => result,
|
|
298
|
-
None => return Err(anyhow::anyhow!("PR number required")),
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
// If the user provides --jq or --web, pass through directly.
|
|
302
|
-
// Note: --json is already handled globally by run() via has_json_flag.
|
|
303
|
-
if should_passthrough_pr_view(&extra_args) {
|
|
304
|
-
return run_passthrough_with_extra("gh", &["pr", "view", &pr_number], &extra_args);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
let mut cmd = Command::new("gh");
|
|
308
|
-
cmd.args([
|
|
309
|
-
"pr",
|
|
310
|
-
"view",
|
|
311
|
-
&pr_number,
|
|
312
|
-
"--json",
|
|
313
|
-
"number,title,state,author,body,url,mergeable,reviews,statusCheckRollup",
|
|
314
|
-
]);
|
|
315
|
-
for arg in &extra_args {
|
|
316
|
-
cmd.arg(arg);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let output = cmd.output().context("Failed to run gh pr view")?;
|
|
320
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
321
|
-
|
|
322
|
-
if !output.status.success() {
|
|
323
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
324
|
-
timer.track(
|
|
325
|
-
&format!("gh pr view {}", pr_number),
|
|
326
|
-
&format!("rtk gh pr view {}", pr_number),
|
|
327
|
-
&stderr,
|
|
328
|
-
&stderr,
|
|
329
|
-
);
|
|
330
|
-
eprintln!("{}", stderr.trim());
|
|
331
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
let json: Value =
|
|
335
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh pr view output")?;
|
|
336
|
-
|
|
337
|
-
let mut filtered = String::new();
|
|
338
|
-
|
|
339
|
-
// Extract essential info
|
|
340
|
-
let number = json["number"].as_i64().unwrap_or(0);
|
|
341
|
-
let title = json["title"].as_str().unwrap_or("???");
|
|
342
|
-
let state = json["state"].as_str().unwrap_or("???");
|
|
343
|
-
let author = json["author"]["login"].as_str().unwrap_or("???");
|
|
344
|
-
let url = json["url"].as_str().unwrap_or("");
|
|
345
|
-
let mergeable = json["mergeable"].as_str().unwrap_or("UNKNOWN");
|
|
346
|
-
|
|
347
|
-
let state_icon = if ultra_compact {
|
|
348
|
-
match state {
|
|
349
|
-
"OPEN" => "O",
|
|
350
|
-
"MERGED" => "M",
|
|
351
|
-
"CLOSED" => "C",
|
|
352
|
-
_ => "?",
|
|
353
|
-
}
|
|
354
|
-
} else {
|
|
355
|
-
match state {
|
|
356
|
-
"OPEN" => "🟢",
|
|
357
|
-
"MERGED" => "🟣",
|
|
358
|
-
"CLOSED" => "🔴",
|
|
359
|
-
_ => "⚪",
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
let line = format!("{} PR #{}: {}\n", state_icon, number, title);
|
|
364
|
-
filtered.push_str(&line);
|
|
365
|
-
print!("{}", line);
|
|
366
|
-
|
|
367
|
-
let line = format!(" {}\n", author);
|
|
368
|
-
filtered.push_str(&line);
|
|
369
|
-
print!("{}", line);
|
|
370
|
-
|
|
371
|
-
let mergeable_str = match mergeable {
|
|
372
|
-
"MERGEABLE" => "✓",
|
|
373
|
-
"CONFLICTING" => "✗",
|
|
374
|
-
_ => "?",
|
|
375
|
-
};
|
|
376
|
-
let line = format!(" {} | {}\n", state, mergeable_str);
|
|
377
|
-
filtered.push_str(&line);
|
|
378
|
-
print!("{}", line);
|
|
379
|
-
|
|
380
|
-
// Show reviews summary
|
|
381
|
-
if let Some(reviews) = json["reviews"]["nodes"].as_array() {
|
|
382
|
-
let approved = reviews
|
|
383
|
-
.iter()
|
|
384
|
-
.filter(|r| r["state"].as_str() == Some("APPROVED"))
|
|
385
|
-
.count();
|
|
386
|
-
let changes = reviews
|
|
387
|
-
.iter()
|
|
388
|
-
.filter(|r| r["state"].as_str() == Some("CHANGES_REQUESTED"))
|
|
389
|
-
.count();
|
|
390
|
-
|
|
391
|
-
if approved > 0 || changes > 0 {
|
|
392
|
-
let line = format!(
|
|
393
|
-
" Reviews: {} approved, {} changes requested\n",
|
|
394
|
-
approved, changes
|
|
395
|
-
);
|
|
396
|
-
filtered.push_str(&line);
|
|
397
|
-
print!("{}", line);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Show checks summary
|
|
402
|
-
if let Some(checks) = json["statusCheckRollup"].as_array() {
|
|
403
|
-
let total = checks.len();
|
|
404
|
-
let passed = checks
|
|
405
|
-
.iter()
|
|
406
|
-
.filter(|c| {
|
|
407
|
-
c["conclusion"].as_str() == Some("SUCCESS")
|
|
408
|
-
|| c["state"].as_str() == Some("SUCCESS")
|
|
409
|
-
})
|
|
410
|
-
.count();
|
|
411
|
-
let failed = checks
|
|
412
|
-
.iter()
|
|
413
|
-
.filter(|c| {
|
|
414
|
-
c["conclusion"].as_str() == Some("FAILURE")
|
|
415
|
-
|| c["state"].as_str() == Some("FAILURE")
|
|
416
|
-
})
|
|
417
|
-
.count();
|
|
418
|
-
|
|
419
|
-
if ultra_compact {
|
|
420
|
-
if failed > 0 {
|
|
421
|
-
let line = format!(" ✗{}/{} {} fail\n", passed, total, failed);
|
|
422
|
-
filtered.push_str(&line);
|
|
423
|
-
print!("{}", line);
|
|
424
|
-
} else {
|
|
425
|
-
let line = format!(" ✓{}/{}\n", passed, total);
|
|
426
|
-
filtered.push_str(&line);
|
|
427
|
-
print!("{}", line);
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
let line = format!(" Checks: {}/{} passed\n", passed, total);
|
|
431
|
-
filtered.push_str(&line);
|
|
432
|
-
print!("{}", line);
|
|
433
|
-
if failed > 0 {
|
|
434
|
-
let line = format!(" ⚠️ {} checks failed\n", failed);
|
|
435
|
-
filtered.push_str(&line);
|
|
436
|
-
print!("{}", line);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
let line = format!(" {}\n", url);
|
|
442
|
-
filtered.push_str(&line);
|
|
443
|
-
print!("{}", line);
|
|
444
|
-
|
|
445
|
-
// Show filtered body
|
|
446
|
-
if let Some(body) = json["body"].as_str() {
|
|
447
|
-
if !body.is_empty() {
|
|
448
|
-
let body_filtered = filter_markdown_body(body);
|
|
449
|
-
if !body_filtered.is_empty() {
|
|
450
|
-
filtered.push('\n');
|
|
451
|
-
println!();
|
|
452
|
-
for line in body_filtered.lines() {
|
|
453
|
-
let formatted = format!(" {}\n", line);
|
|
454
|
-
filtered.push_str(&formatted);
|
|
455
|
-
print!("{}", formatted);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
timer.track(
|
|
462
|
-
&format!("gh pr view {}", pr_number),
|
|
463
|
-
&format!("rtk gh pr view {}", pr_number),
|
|
464
|
-
&raw,
|
|
465
|
-
&filtered,
|
|
466
|
-
);
|
|
467
|
-
Ok(())
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
fn pr_checks(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()> {
|
|
471
|
-
let timer = tracking::TimedExecution::start();
|
|
472
|
-
|
|
473
|
-
let (pr_number, extra_args) = match extract_identifier_and_extra_args(args) {
|
|
474
|
-
Some(result) => result,
|
|
475
|
-
None => return Err(anyhow::anyhow!("PR number required")),
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
let mut cmd = Command::new("gh");
|
|
479
|
-
cmd.args(["pr", "checks", &pr_number]);
|
|
480
|
-
for arg in &extra_args {
|
|
481
|
-
cmd.arg(arg);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
let output = cmd.output().context("Failed to run gh pr checks")?;
|
|
485
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
486
|
-
|
|
487
|
-
if !output.status.success() {
|
|
488
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
489
|
-
timer.track(
|
|
490
|
-
&format!("gh pr checks {}", pr_number),
|
|
491
|
-
&format!("rtk gh pr checks {}", pr_number),
|
|
492
|
-
&stderr,
|
|
493
|
-
&stderr,
|
|
494
|
-
);
|
|
495
|
-
eprintln!("{}", stderr.trim());
|
|
496
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
500
|
-
|
|
501
|
-
// Parse and compress checks output
|
|
502
|
-
let mut passed = 0;
|
|
503
|
-
let mut failed = 0;
|
|
504
|
-
let mut pending = 0;
|
|
505
|
-
let mut failed_checks = Vec::new();
|
|
506
|
-
|
|
507
|
-
for line in stdout.lines() {
|
|
508
|
-
if line.contains('✓') || line.contains("pass") {
|
|
509
|
-
passed += 1;
|
|
510
|
-
} else if line.contains('✗') || line.contains("fail") {
|
|
511
|
-
failed += 1;
|
|
512
|
-
failed_checks.push(line.trim().to_string());
|
|
513
|
-
} else if line.contains('*') || line.contains("pending") {
|
|
514
|
-
pending += 1;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
let mut filtered = String::new();
|
|
519
|
-
|
|
520
|
-
let line = "🔍 CI Checks Summary:\n";
|
|
521
|
-
filtered.push_str(line);
|
|
522
|
-
print!("{}", line);
|
|
523
|
-
|
|
524
|
-
let line = format!(" ✅ Passed: {}\n", passed);
|
|
525
|
-
filtered.push_str(&line);
|
|
526
|
-
print!("{}", line);
|
|
527
|
-
|
|
528
|
-
let line = format!(" ❌ Failed: {}\n", failed);
|
|
529
|
-
filtered.push_str(&line);
|
|
530
|
-
print!("{}", line);
|
|
531
|
-
|
|
532
|
-
if pending > 0 {
|
|
533
|
-
let line = format!(" ⏳ Pending: {}\n", pending);
|
|
534
|
-
filtered.push_str(&line);
|
|
535
|
-
print!("{}", line);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if !failed_checks.is_empty() {
|
|
539
|
-
let line = "\n Failed checks:\n";
|
|
540
|
-
filtered.push_str(line);
|
|
541
|
-
print!("{}", line);
|
|
542
|
-
for check in failed_checks {
|
|
543
|
-
let line = format!(" {}\n", check);
|
|
544
|
-
filtered.push_str(&line);
|
|
545
|
-
print!("{}", line);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
timer.track(
|
|
550
|
-
&format!("gh pr checks {}", pr_number),
|
|
551
|
-
&format!("rtk gh pr checks {}", pr_number),
|
|
552
|
-
&raw,
|
|
553
|
-
&filtered,
|
|
554
|
-
);
|
|
555
|
-
Ok(())
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
fn pr_status(_verbose: u8, _ultra_compact: bool) -> Result<()> {
|
|
559
|
-
let timer = tracking::TimedExecution::start();
|
|
560
|
-
|
|
561
|
-
let mut cmd = Command::new("gh");
|
|
562
|
-
cmd.args([
|
|
563
|
-
"pr",
|
|
564
|
-
"status",
|
|
565
|
-
"--json",
|
|
566
|
-
"currentBranch,createdBy,reviewDecision,statusCheckRollup",
|
|
567
|
-
]);
|
|
568
|
-
|
|
569
|
-
let output = cmd.output().context("Failed to run gh pr status")?;
|
|
570
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
571
|
-
|
|
572
|
-
if !output.status.success() {
|
|
573
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
574
|
-
timer.track("gh pr status", "rtk gh pr status", &stderr, &stderr);
|
|
575
|
-
eprintln!("{}", stderr.trim());
|
|
576
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
let json: Value =
|
|
580
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh pr status output")?;
|
|
581
|
-
|
|
582
|
-
let mut filtered = String::new();
|
|
583
|
-
|
|
584
|
-
if let Some(created_by) = json["createdBy"].as_array() {
|
|
585
|
-
let line = format!("📝 Your PRs ({}):\n", created_by.len());
|
|
586
|
-
filtered.push_str(&line);
|
|
587
|
-
print!("{}", line);
|
|
588
|
-
for pr in created_by.iter().take(5) {
|
|
589
|
-
let number = pr["number"].as_i64().unwrap_or(0);
|
|
590
|
-
let title = pr["title"].as_str().unwrap_or("???");
|
|
591
|
-
let reviews = pr["reviewDecision"].as_str().unwrap_or("PENDING");
|
|
592
|
-
let line = format!(" #{} {} [{}]\n", number, truncate(title, 50), reviews);
|
|
593
|
-
filtered.push_str(&line);
|
|
594
|
-
print!("{}", line);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
timer.track("gh pr status", "rtk gh pr status", &raw, &filtered);
|
|
599
|
-
Ok(())
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
fn run_issue(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
603
|
-
if args.is_empty() {
|
|
604
|
-
return run_passthrough("gh", "issue", args);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
match args[0].as_str() {
|
|
608
|
-
"list" => list_issues(&args[1..], verbose, ultra_compact),
|
|
609
|
-
"view" => view_issue(&args[1..], verbose),
|
|
610
|
-
_ => run_passthrough("gh", "issue", args),
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
fn list_issues(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
615
|
-
let timer = tracking::TimedExecution::start();
|
|
616
|
-
|
|
617
|
-
let mut cmd = Command::new("gh");
|
|
618
|
-
cmd.args(["issue", "list", "--json", "number,title,state,author"]);
|
|
619
|
-
|
|
620
|
-
for arg in args {
|
|
621
|
-
cmd.arg(arg);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
let output = cmd.output().context("Failed to run gh issue list")?;
|
|
625
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
626
|
-
|
|
627
|
-
if !output.status.success() {
|
|
628
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
629
|
-
timer.track("gh issue list", "rtk gh issue list", &stderr, &stderr);
|
|
630
|
-
eprintln!("{}", stderr.trim());
|
|
631
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
let json: Value =
|
|
635
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh issue list output")?;
|
|
636
|
-
|
|
637
|
-
let mut filtered = String::new();
|
|
638
|
-
|
|
639
|
-
if let Some(issues) = json.as_array() {
|
|
640
|
-
if ultra_compact {
|
|
641
|
-
filtered.push_str("Issues\n");
|
|
642
|
-
println!("Issues");
|
|
643
|
-
} else {
|
|
644
|
-
filtered.push_str("🐛 Issues\n");
|
|
645
|
-
println!("🐛 Issues");
|
|
646
|
-
}
|
|
647
|
-
for issue in issues.iter().take(20) {
|
|
648
|
-
let number = issue["number"].as_i64().unwrap_or(0);
|
|
649
|
-
let title = issue["title"].as_str().unwrap_or("???");
|
|
650
|
-
let state = issue["state"].as_str().unwrap_or("???");
|
|
651
|
-
|
|
652
|
-
let icon = if ultra_compact {
|
|
653
|
-
if state == "OPEN" {
|
|
654
|
-
"O"
|
|
655
|
-
} else {
|
|
656
|
-
"C"
|
|
657
|
-
}
|
|
658
|
-
} else {
|
|
659
|
-
if state == "OPEN" {
|
|
660
|
-
"🟢"
|
|
661
|
-
} else {
|
|
662
|
-
"🔴"
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
let line = format!(" {} #{} {}\n", icon, number, truncate(title, 60));
|
|
666
|
-
filtered.push_str(&line);
|
|
667
|
-
print!("{}", line);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if issues.len() > 20 {
|
|
671
|
-
let line = format!(" ... {} more\n", issues.len() - 20);
|
|
672
|
-
filtered.push_str(&line);
|
|
673
|
-
print!("{}", line);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
timer.track("gh issue list", "rtk gh issue list", &raw, &filtered);
|
|
678
|
-
Ok(())
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
fn view_issue(args: &[String], _verbose: u8) -> Result<()> {
|
|
682
|
-
let timer = tracking::TimedExecution::start();
|
|
683
|
-
|
|
684
|
-
let (issue_number, extra_args) = match extract_identifier_and_extra_args(args) {
|
|
685
|
-
Some(result) => result,
|
|
686
|
-
None => return Err(anyhow::anyhow!("Issue number required")),
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
let mut cmd = Command::new("gh");
|
|
690
|
-
cmd.args([
|
|
691
|
-
"issue",
|
|
692
|
-
"view",
|
|
693
|
-
&issue_number,
|
|
694
|
-
"--json",
|
|
695
|
-
"number,title,state,author,body,url",
|
|
696
|
-
]);
|
|
697
|
-
for arg in &extra_args {
|
|
698
|
-
cmd.arg(arg);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
let output = cmd.output().context("Failed to run gh issue view")?;
|
|
702
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
703
|
-
|
|
704
|
-
if !output.status.success() {
|
|
705
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
706
|
-
timer.track(
|
|
707
|
-
&format!("gh issue view {}", issue_number),
|
|
708
|
-
&format!("rtk gh issue view {}", issue_number),
|
|
709
|
-
&stderr,
|
|
710
|
-
&stderr,
|
|
711
|
-
);
|
|
712
|
-
eprintln!("{}", stderr.trim());
|
|
713
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
let json: Value =
|
|
717
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh issue view output")?;
|
|
718
|
-
|
|
719
|
-
let number = json["number"].as_i64().unwrap_or(0);
|
|
720
|
-
let title = json["title"].as_str().unwrap_or("???");
|
|
721
|
-
let state = json["state"].as_str().unwrap_or("???");
|
|
722
|
-
let author = json["author"]["login"].as_str().unwrap_or("???");
|
|
723
|
-
let url = json["url"].as_str().unwrap_or("");
|
|
724
|
-
|
|
725
|
-
let icon = if state == "OPEN" { "🟢" } else { "🔴" };
|
|
726
|
-
|
|
727
|
-
let mut filtered = String::new();
|
|
728
|
-
|
|
729
|
-
let line = format!("{} Issue #{}: {}\n", icon, number, title);
|
|
730
|
-
filtered.push_str(&line);
|
|
731
|
-
print!("{}", line);
|
|
732
|
-
|
|
733
|
-
let line = format!(" Author: @{}\n", author);
|
|
734
|
-
filtered.push_str(&line);
|
|
735
|
-
print!("{}", line);
|
|
736
|
-
|
|
737
|
-
let line = format!(" Status: {}\n", state);
|
|
738
|
-
filtered.push_str(&line);
|
|
739
|
-
print!("{}", line);
|
|
740
|
-
|
|
741
|
-
let line = format!(" URL: {}\n", url);
|
|
742
|
-
filtered.push_str(&line);
|
|
743
|
-
print!("{}", line);
|
|
744
|
-
|
|
745
|
-
if let Some(body) = json["body"].as_str() {
|
|
746
|
-
if !body.is_empty() {
|
|
747
|
-
let body_filtered = filter_markdown_body(body);
|
|
748
|
-
if !body_filtered.is_empty() {
|
|
749
|
-
let line = "\n Description:\n";
|
|
750
|
-
filtered.push_str(line);
|
|
751
|
-
print!("{}", line);
|
|
752
|
-
for line in body_filtered.lines() {
|
|
753
|
-
let formatted = format!(" {}\n", line);
|
|
754
|
-
filtered.push_str(&formatted);
|
|
755
|
-
print!("{}", formatted);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
timer.track(
|
|
762
|
-
&format!("gh issue view {}", issue_number),
|
|
763
|
-
&format!("rtk gh issue view {}", issue_number),
|
|
764
|
-
&raw,
|
|
765
|
-
&filtered,
|
|
766
|
-
);
|
|
767
|
-
Ok(())
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
fn run_workflow(args: &[String], verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
771
|
-
if args.is_empty() {
|
|
772
|
-
return run_passthrough("gh", "run", args);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
match args[0].as_str() {
|
|
776
|
-
"list" => list_runs(&args[1..], verbose, ultra_compact),
|
|
777
|
-
"view" => view_run(&args[1..], verbose),
|
|
778
|
-
_ => run_passthrough("gh", "run", args),
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
fn list_runs(args: &[String], _verbose: u8, ultra_compact: bool) -> Result<()> {
|
|
783
|
-
let timer = tracking::TimedExecution::start();
|
|
784
|
-
|
|
785
|
-
let mut cmd = Command::new("gh");
|
|
786
|
-
cmd.args([
|
|
787
|
-
"run",
|
|
788
|
-
"list",
|
|
789
|
-
"--json",
|
|
790
|
-
"databaseId,name,status,conclusion,createdAt",
|
|
791
|
-
]);
|
|
792
|
-
cmd.arg("--limit").arg("10");
|
|
793
|
-
|
|
794
|
-
for arg in args {
|
|
795
|
-
cmd.arg(arg);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
let output = cmd.output().context("Failed to run gh run list")?;
|
|
799
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
800
|
-
|
|
801
|
-
if !output.status.success() {
|
|
802
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
803
|
-
timer.track("gh run list", "rtk gh run list", &stderr, &stderr);
|
|
804
|
-
eprintln!("{}", stderr.trim());
|
|
805
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
let json: Value =
|
|
809
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh run list output")?;
|
|
810
|
-
|
|
811
|
-
let mut filtered = String::new();
|
|
812
|
-
|
|
813
|
-
if let Some(runs) = json.as_array() {
|
|
814
|
-
if ultra_compact {
|
|
815
|
-
filtered.push_str("Runs\n");
|
|
816
|
-
println!("Runs");
|
|
817
|
-
} else {
|
|
818
|
-
filtered.push_str("🏃 Workflow Runs\n");
|
|
819
|
-
println!("🏃 Workflow Runs");
|
|
820
|
-
}
|
|
821
|
-
for run in runs {
|
|
822
|
-
let id = run["databaseId"].as_i64().unwrap_or(0);
|
|
823
|
-
let name = run["name"].as_str().unwrap_or("???");
|
|
824
|
-
let status = run["status"].as_str().unwrap_or("???");
|
|
825
|
-
let conclusion = run["conclusion"].as_str().unwrap_or("");
|
|
826
|
-
|
|
827
|
-
let icon = if ultra_compact {
|
|
828
|
-
match conclusion {
|
|
829
|
-
"success" => "✓",
|
|
830
|
-
"failure" => "✗",
|
|
831
|
-
"cancelled" => "X",
|
|
832
|
-
_ => {
|
|
833
|
-
if status == "in_progress" {
|
|
834
|
-
"~"
|
|
835
|
-
} else {
|
|
836
|
-
"?"
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
} else {
|
|
841
|
-
match conclusion {
|
|
842
|
-
"success" => "✅",
|
|
843
|
-
"failure" => "❌",
|
|
844
|
-
"cancelled" => "🚫",
|
|
845
|
-
_ => {
|
|
846
|
-
if status == "in_progress" {
|
|
847
|
-
"⏳"
|
|
848
|
-
} else {
|
|
849
|
-
"⚪"
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
};
|
|
854
|
-
|
|
855
|
-
let line = format!(" {} {} [{}]\n", icon, truncate(name, 50), id);
|
|
856
|
-
filtered.push_str(&line);
|
|
857
|
-
print!("{}", line);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
timer.track("gh run list", "rtk gh run list", &raw, &filtered);
|
|
862
|
-
Ok(())
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/// Check if run view args should bypass filtering and pass through directly.
|
|
866
|
-
/// Flags like --log-failed, --log, and --json produce output that the filter
|
|
867
|
-
/// would incorrectly strip.
|
|
868
|
-
fn should_passthrough_run_view(extra_args: &[String]) -> bool {
|
|
869
|
-
extra_args
|
|
870
|
-
.iter()
|
|
871
|
-
.any(|a| a == "--log-failed" || a == "--log" || a == "--json")
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
fn view_run(args: &[String], _verbose: u8) -> Result<()> {
|
|
875
|
-
let (run_id, extra_args) = match extract_identifier_and_extra_args(args) {
|
|
876
|
-
Some(result) => result,
|
|
877
|
-
None => return Err(anyhow::anyhow!("Run ID required")),
|
|
878
|
-
};
|
|
879
|
-
|
|
880
|
-
// Pass through when user requests logs or JSON — the filter would strip them
|
|
881
|
-
if should_passthrough_run_view(&extra_args) {
|
|
882
|
-
return run_passthrough_with_extra("gh", &["run", "view", &run_id], &extra_args);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
let timer = tracking::TimedExecution::start();
|
|
886
|
-
|
|
887
|
-
let mut cmd = Command::new("gh");
|
|
888
|
-
cmd.args(["run", "view", &run_id]);
|
|
889
|
-
for arg in &extra_args {
|
|
890
|
-
cmd.arg(arg);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
let output = cmd.output().context("Failed to run gh run view")?;
|
|
894
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
895
|
-
|
|
896
|
-
if !output.status.success() {
|
|
897
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
898
|
-
timer.track(
|
|
899
|
-
&format!("gh run view {}", run_id),
|
|
900
|
-
&format!("rtk gh run view {}", run_id),
|
|
901
|
-
&stderr,
|
|
902
|
-
&stderr,
|
|
903
|
-
);
|
|
904
|
-
eprintln!("{}", stderr.trim());
|
|
905
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Parse output and show only failures
|
|
909
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
910
|
-
let mut in_jobs = false;
|
|
911
|
-
|
|
912
|
-
let mut filtered = String::new();
|
|
913
|
-
|
|
914
|
-
let line = format!("🏃 Workflow Run #{}\n", run_id);
|
|
915
|
-
filtered.push_str(&line);
|
|
916
|
-
print!("{}", line);
|
|
917
|
-
|
|
918
|
-
for line in stdout.lines() {
|
|
919
|
-
if line.contains("JOBS") {
|
|
920
|
-
in_jobs = true;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if in_jobs {
|
|
924
|
-
if line.contains('✓') || line.contains("success") {
|
|
925
|
-
// Skip successful jobs in compact mode
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
if line.contains('✗') || line.contains("fail") {
|
|
929
|
-
let formatted = format!(" ❌ {}\n", line.trim());
|
|
930
|
-
filtered.push_str(&formatted);
|
|
931
|
-
print!("{}", formatted);
|
|
932
|
-
}
|
|
933
|
-
} else if line.contains("Status:") || line.contains("Conclusion:") {
|
|
934
|
-
let formatted = format!(" {}\n", line.trim());
|
|
935
|
-
filtered.push_str(&formatted);
|
|
936
|
-
print!("{}", formatted);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
timer.track(
|
|
941
|
-
&format!("gh run view {}", run_id),
|
|
942
|
-
&format!("rtk gh run view {}", run_id),
|
|
943
|
-
&raw,
|
|
944
|
-
&filtered,
|
|
945
|
-
);
|
|
946
|
-
Ok(())
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
fn run_repo(args: &[String], _verbose: u8, _ultra_compact: bool) -> Result<()> {
|
|
950
|
-
// Parse subcommand (default to "view")
|
|
951
|
-
let (subcommand, rest_args) = if args.is_empty() {
|
|
952
|
-
("view", args)
|
|
953
|
-
} else {
|
|
954
|
-
(args[0].as_str(), &args[1..])
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
if subcommand != "view" {
|
|
958
|
-
return run_passthrough("gh", "repo", args);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
let timer = tracking::TimedExecution::start();
|
|
962
|
-
|
|
963
|
-
let mut cmd = Command::new("gh");
|
|
964
|
-
cmd.arg("repo").arg("view");
|
|
965
|
-
|
|
966
|
-
for arg in rest_args {
|
|
967
|
-
cmd.arg(arg);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
cmd.args([
|
|
971
|
-
"--json",
|
|
972
|
-
"name,owner,description,url,stargazerCount,forkCount,isPrivate",
|
|
973
|
-
]);
|
|
974
|
-
|
|
975
|
-
let output = cmd.output().context("Failed to run gh repo view")?;
|
|
976
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
977
|
-
|
|
978
|
-
if !output.status.success() {
|
|
979
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
980
|
-
timer.track("gh repo view", "rtk gh repo view", &stderr, &stderr);
|
|
981
|
-
eprintln!("{}", stderr.trim());
|
|
982
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
let json: Value =
|
|
986
|
-
serde_json::from_slice(&output.stdout).context("Failed to parse gh repo view output")?;
|
|
987
|
-
|
|
988
|
-
let name = json["name"].as_str().unwrap_or("???");
|
|
989
|
-
let owner = json["owner"]["login"].as_str().unwrap_or("???");
|
|
990
|
-
let description = json["description"].as_str().unwrap_or("");
|
|
991
|
-
let url = json["url"].as_str().unwrap_or("");
|
|
992
|
-
let stars = json["stargazerCount"].as_i64().unwrap_or(0);
|
|
993
|
-
let forks = json["forkCount"].as_i64().unwrap_or(0);
|
|
994
|
-
let private = json["isPrivate"].as_bool().unwrap_or(false);
|
|
995
|
-
|
|
996
|
-
let visibility = if private {
|
|
997
|
-
"🔒 Private"
|
|
998
|
-
} else {
|
|
999
|
-
"🌐 Public"
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
let mut filtered = String::new();
|
|
1003
|
-
|
|
1004
|
-
let line = format!("📦 {}/{}\n", owner, name);
|
|
1005
|
-
filtered.push_str(&line);
|
|
1006
|
-
print!("{}", line);
|
|
1007
|
-
|
|
1008
|
-
let line = format!(" {}\n", visibility);
|
|
1009
|
-
filtered.push_str(&line);
|
|
1010
|
-
print!("{}", line);
|
|
1011
|
-
|
|
1012
|
-
if !description.is_empty() {
|
|
1013
|
-
let line = format!(" {}\n", truncate(description, 80));
|
|
1014
|
-
filtered.push_str(&line);
|
|
1015
|
-
print!("{}", line);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
let line = format!(" ⭐ {} stars | 🔱 {} forks\n", stars, forks);
|
|
1019
|
-
filtered.push_str(&line);
|
|
1020
|
-
print!("{}", line);
|
|
1021
|
-
|
|
1022
|
-
let line = format!(" {}\n", url);
|
|
1023
|
-
filtered.push_str(&line);
|
|
1024
|
-
print!("{}", line);
|
|
1025
|
-
|
|
1026
|
-
timer.track("gh repo view", "rtk gh repo view", &raw, &filtered);
|
|
1027
|
-
Ok(())
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
fn pr_create(args: &[String], _verbose: u8) -> Result<()> {
|
|
1031
|
-
let timer = tracking::TimedExecution::start();
|
|
1032
|
-
|
|
1033
|
-
let mut cmd = Command::new("gh");
|
|
1034
|
-
cmd.args(["pr", "create"]);
|
|
1035
|
-
for arg in args {
|
|
1036
|
-
cmd.arg(arg);
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
let output = cmd.output().context("Failed to run gh pr create")?;
|
|
1040
|
-
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
1041
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
1042
|
-
|
|
1043
|
-
if !output.status.success() {
|
|
1044
|
-
timer.track("gh pr create", "rtk gh pr create", &stderr, &stderr);
|
|
1045
|
-
eprintln!("{}", stderr.trim());
|
|
1046
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// gh pr create outputs the URL on success
|
|
1050
|
-
let url = stdout.trim();
|
|
1051
|
-
|
|
1052
|
-
// Try to extract PR number from URL (e.g., https://github.com/owner/repo/pull/42)
|
|
1053
|
-
let pr_num = url.rsplit('/').next().unwrap_or("");
|
|
1054
|
-
|
|
1055
|
-
let detail = if !pr_num.is_empty() && pr_num.chars().all(|c| c.is_ascii_digit()) {
|
|
1056
|
-
format!("#{} {}", pr_num, url)
|
|
1057
|
-
} else {
|
|
1058
|
-
url.to_string()
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
let filtered = ok_confirmation("created", &detail);
|
|
1062
|
-
println!("{}", filtered);
|
|
1063
|
-
|
|
1064
|
-
timer.track("gh pr create", "rtk gh pr create", &stdout, &filtered);
|
|
1065
|
-
Ok(())
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
fn pr_merge(args: &[String], _verbose: u8) -> Result<()> {
|
|
1069
|
-
let timer = tracking::TimedExecution::start();
|
|
1070
|
-
|
|
1071
|
-
let mut cmd = Command::new("gh");
|
|
1072
|
-
cmd.args(["pr", "merge"]);
|
|
1073
|
-
for arg in args {
|
|
1074
|
-
cmd.arg(arg);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
let output = cmd.output().context("Failed to run gh pr merge")?;
|
|
1078
|
-
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
1079
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
1080
|
-
|
|
1081
|
-
if !output.status.success() {
|
|
1082
|
-
timer.track("gh pr merge", "rtk gh pr merge", &stderr, &stderr);
|
|
1083
|
-
eprintln!("{}", stderr.trim());
|
|
1084
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// Extract PR number from args (first non-flag arg)
|
|
1088
|
-
let pr_num = args
|
|
1089
|
-
.iter()
|
|
1090
|
-
.find(|a| !a.starts_with('-'))
|
|
1091
|
-
.map(|s| s.as_str())
|
|
1092
|
-
.unwrap_or("");
|
|
1093
|
-
|
|
1094
|
-
let detail = if !pr_num.is_empty() {
|
|
1095
|
-
format!("#{}", pr_num)
|
|
1096
|
-
} else {
|
|
1097
|
-
String::new()
|
|
1098
|
-
};
|
|
1099
|
-
|
|
1100
|
-
let filtered = ok_confirmation("merged", &detail);
|
|
1101
|
-
println!("{}", filtered);
|
|
1102
|
-
|
|
1103
|
-
// Use stdout or detail as raw input (gh pr merge doesn't output much)
|
|
1104
|
-
let raw = if !stdout.trim().is_empty() {
|
|
1105
|
-
stdout
|
|
1106
|
-
} else {
|
|
1107
|
-
detail.clone()
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
|
-
timer.track("gh pr merge", "rtk gh pr merge", &raw, &filtered);
|
|
1111
|
-
Ok(())
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
fn pr_diff(args: &[String], _verbose: u8) -> Result<()> {
|
|
1115
|
-
// --no-compact: pass full diff through (gh CLI doesn't know this flag, strip it)
|
|
1116
|
-
let no_compact = args.iter().any(|a| a == "--no-compact");
|
|
1117
|
-
let gh_args: Vec<String> = args
|
|
1118
|
-
.iter()
|
|
1119
|
-
.filter(|a| *a != "--no-compact")
|
|
1120
|
-
.cloned()
|
|
1121
|
-
.collect();
|
|
1122
|
-
|
|
1123
|
-
if no_compact {
|
|
1124
|
-
return run_passthrough_with_extra("gh", &["pr", "diff"], &gh_args);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
let timer = tracking::TimedExecution::start();
|
|
1128
|
-
|
|
1129
|
-
let mut cmd = Command::new("gh");
|
|
1130
|
-
cmd.args(["pr", "diff"]);
|
|
1131
|
-
for arg in gh_args.iter() {
|
|
1132
|
-
cmd.arg(arg);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
let output = cmd.output().context("Failed to run gh pr diff")?;
|
|
1136
|
-
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
1137
|
-
|
|
1138
|
-
if !output.status.success() {
|
|
1139
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
1140
|
-
timer.track("gh pr diff", "rtk gh pr diff", &stderr, &stderr);
|
|
1141
|
-
eprintln!("{}", stderr.trim());
|
|
1142
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
let filtered = if raw.trim().is_empty() {
|
|
1146
|
-
let msg = "No diff\n";
|
|
1147
|
-
print!("{}", msg);
|
|
1148
|
-
msg.to_string()
|
|
1149
|
-
} else {
|
|
1150
|
-
let compacted = git::compact_diff(&raw, 500);
|
|
1151
|
-
println!("{}", compacted);
|
|
1152
|
-
compacted
|
|
1153
|
-
};
|
|
1154
|
-
|
|
1155
|
-
timer.track("gh pr diff", "rtk gh pr diff", &raw, &filtered);
|
|
1156
|
-
Ok(())
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/// Generic PR action handler for comment/edit
|
|
1160
|
-
fn pr_action(action: &str, args: &[String], _verbose: u8) -> Result<()> {
|
|
1161
|
-
let timer = tracking::TimedExecution::start();
|
|
1162
|
-
let subcmd = &args[0];
|
|
1163
|
-
|
|
1164
|
-
let mut cmd = Command::new("gh");
|
|
1165
|
-
cmd.arg("pr");
|
|
1166
|
-
for arg in args {
|
|
1167
|
-
cmd.arg(arg);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
let output = cmd
|
|
1171
|
-
.output()
|
|
1172
|
-
.context(format!("Failed to run gh pr {}", subcmd))?;
|
|
1173
|
-
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
1174
|
-
|
|
1175
|
-
if !output.status.success() {
|
|
1176
|
-
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
1177
|
-
timer.track(
|
|
1178
|
-
&format!("gh pr {}", subcmd),
|
|
1179
|
-
&format!("rtk gh pr {}", subcmd),
|
|
1180
|
-
&stderr,
|
|
1181
|
-
&stderr,
|
|
1182
|
-
);
|
|
1183
|
-
eprintln!("{}", stderr.trim());
|
|
1184
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Extract PR number from args (skip args[0] which is the subcommand)
|
|
1188
|
-
let pr_num = args[1..]
|
|
1189
|
-
.iter()
|
|
1190
|
-
.find(|a| !a.starts_with('-'))
|
|
1191
|
-
.map(|s| format!("#{}", s))
|
|
1192
|
-
.unwrap_or_default();
|
|
1193
|
-
|
|
1194
|
-
let filtered = ok_confirmation(action, &pr_num);
|
|
1195
|
-
println!("{}", filtered);
|
|
1196
|
-
|
|
1197
|
-
// Use stdout or pr_num as raw input
|
|
1198
|
-
let raw = if !stdout.trim().is_empty() {
|
|
1199
|
-
stdout
|
|
1200
|
-
} else {
|
|
1201
|
-
pr_num.clone()
|
|
1202
|
-
};
|
|
1203
|
-
|
|
1204
|
-
timer.track(
|
|
1205
|
-
&format!("gh pr {}", subcmd),
|
|
1206
|
-
&format!("rtk gh pr {}", subcmd),
|
|
1207
|
-
&raw,
|
|
1208
|
-
&filtered,
|
|
1209
|
-
);
|
|
1210
|
-
Ok(())
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
fn run_api(args: &[String], _verbose: u8) -> Result<()> {
|
|
1214
|
-
// gh api is an explicit/advanced command — the user knows what they asked for.
|
|
1215
|
-
// Converting JSON to a schema destroys all values and forces Claude to re-fetch.
|
|
1216
|
-
// Passthrough preserves the full response and tracks metrics at 0% savings.
|
|
1217
|
-
run_passthrough("gh", "api", args)
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/// Pass through a command with base args + extra args, tracking as passthrough.
|
|
1221
|
-
fn run_passthrough_with_extra(cmd: &str, base_args: &[&str], extra_args: &[String]) -> Result<()> {
|
|
1222
|
-
let timer = tracking::TimedExecution::start();
|
|
1223
|
-
|
|
1224
|
-
let mut command = Command::new(cmd);
|
|
1225
|
-
for arg in base_args {
|
|
1226
|
-
command.arg(arg);
|
|
1227
|
-
}
|
|
1228
|
-
for arg in extra_args {
|
|
1229
|
-
command.arg(arg);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
let status =
|
|
1233
|
-
command
|
|
1234
|
-
.status()
|
|
1235
|
-
.context(format!("Failed to run {} {}", cmd, base_args.join(" ")))?;
|
|
1236
|
-
|
|
1237
|
-
let full_cmd = format!(
|
|
1238
|
-
"{} {} {}",
|
|
1239
|
-
cmd,
|
|
1240
|
-
base_args.join(" "),
|
|
1241
|
-
tracking::args_display(&extra_args.iter().map(|s| s.into()).collect::<Vec<_>>())
|
|
1242
|
-
);
|
|
1243
|
-
timer.track_passthrough(&full_cmd, &format!("rtk {} (passthrough)", full_cmd));
|
|
1244
|
-
|
|
1245
|
-
if !status.success() {
|
|
1246
|
-
std::process::exit(status.code().unwrap_or(1));
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
Ok(())
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
fn run_passthrough(cmd: &str, subcommand: &str, args: &[String]) -> Result<()> {
|
|
1253
|
-
let timer = tracking::TimedExecution::start();
|
|
1254
|
-
|
|
1255
|
-
let mut command = Command::new(cmd);
|
|
1256
|
-
command.arg(subcommand);
|
|
1257
|
-
for arg in args {
|
|
1258
|
-
command.arg(arg);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
let status = command
|
|
1262
|
-
.status()
|
|
1263
|
-
.context(format!("Failed to run {} {}", cmd, subcommand))?;
|
|
1264
|
-
|
|
1265
|
-
let args_str = tracking::args_display(&args.iter().map(|s| s.into()).collect::<Vec<_>>());
|
|
1266
|
-
timer.track_passthrough(
|
|
1267
|
-
&format!("{} {} {}", cmd, subcommand, args_str),
|
|
1268
|
-
&format!("rtk {} {} {} (passthrough)", cmd, subcommand, args_str),
|
|
1269
|
-
);
|
|
1270
|
-
|
|
1271
|
-
if !status.success() {
|
|
1272
|
-
std::process::exit(status.code().unwrap_or(1));
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
Ok(())
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
#[cfg(test)]
|
|
1279
|
-
mod tests {
|
|
1280
|
-
use super::*;
|
|
1281
|
-
|
|
1282
|
-
#[test]
|
|
1283
|
-
fn test_truncate() {
|
|
1284
|
-
assert_eq!(truncate("short", 10), "short");
|
|
1285
|
-
assert_eq!(
|
|
1286
|
-
truncate("this is a very long string", 15),
|
|
1287
|
-
"this is a ve..."
|
|
1288
|
-
);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
#[test]
|
|
1292
|
-
fn test_truncate_multibyte_utf8() {
|
|
1293
|
-
// Emoji: 🚀 = 4 bytes, 1 char
|
|
1294
|
-
assert_eq!(truncate("🚀🎉🔥abc", 6), "🚀🎉🔥abc"); // 6 chars, fits
|
|
1295
|
-
assert_eq!(truncate("🚀🎉🔥abcdef", 8), "🚀🎉🔥ab..."); // 10 chars > 8
|
|
1296
|
-
// Edge case: all multibyte
|
|
1297
|
-
assert_eq!(truncate("🚀🎉🔥🌟🎯", 5), "🚀🎉🔥🌟🎯"); // exact fit
|
|
1298
|
-
assert_eq!(truncate("🚀🎉🔥🌟🎯x", 5), "🚀🎉..."); // 6 chars > 5
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
#[test]
|
|
1302
|
-
fn test_truncate_empty_and_short() {
|
|
1303
|
-
assert_eq!(truncate("", 10), "");
|
|
1304
|
-
assert_eq!(truncate("ab", 10), "ab");
|
|
1305
|
-
assert_eq!(truncate("abc", 3), "abc"); // exact fit
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
#[test]
|
|
1309
|
-
fn test_ok_confirmation_pr_create() {
|
|
1310
|
-
let result = ok_confirmation("created", "#42 https://github.com/foo/bar/pull/42");
|
|
1311
|
-
assert!(result.contains("ok created"));
|
|
1312
|
-
assert!(result.contains("#42"));
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
#[test]
|
|
1316
|
-
fn test_ok_confirmation_pr_merge() {
|
|
1317
|
-
let result = ok_confirmation("merged", "#42");
|
|
1318
|
-
assert_eq!(result, "ok merged #42");
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
#[test]
|
|
1322
|
-
fn test_ok_confirmation_pr_comment() {
|
|
1323
|
-
let result = ok_confirmation("commented", "#42");
|
|
1324
|
-
assert_eq!(result, "ok commented #42");
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
#[test]
|
|
1328
|
-
fn test_ok_confirmation_pr_edit() {
|
|
1329
|
-
let result = ok_confirmation("edited", "#42");
|
|
1330
|
-
assert_eq!(result, "ok edited #42");
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
#[test]
|
|
1334
|
-
fn test_has_json_flag_present() {
|
|
1335
|
-
assert!(has_json_flag(&[
|
|
1336
|
-
"view".into(),
|
|
1337
|
-
"--json".into(),
|
|
1338
|
-
"number,url".into()
|
|
1339
|
-
]));
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
#[test]
|
|
1343
|
-
fn test_has_json_flag_absent() {
|
|
1344
|
-
assert!(!has_json_flag(&["view".into(), "42".into()]));
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
#[test]
|
|
1348
|
-
fn test_extract_identifier_simple() {
|
|
1349
|
-
let args: Vec<String> = vec!["123".into()];
|
|
1350
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1351
|
-
assert_eq!(id, "123");
|
|
1352
|
-
assert!(extra.is_empty());
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
#[test]
|
|
1356
|
-
fn test_extract_identifier_with_repo_flag_after() {
|
|
1357
|
-
// gh issue view 185 -R rtk-ai/rtk
|
|
1358
|
-
let args: Vec<String> = vec!["185".into(), "-R".into(), "rtk-ai/rtk".into()];
|
|
1359
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1360
|
-
assert_eq!(id, "185");
|
|
1361
|
-
assert_eq!(extra, vec!["-R", "rtk-ai/rtk"]);
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
#[test]
|
|
1365
|
-
fn test_extract_identifier_with_repo_flag_before() {
|
|
1366
|
-
// gh issue view -R rtk-ai/rtk 185
|
|
1367
|
-
let args: Vec<String> = vec!["-R".into(), "rtk-ai/rtk".into(), "185".into()];
|
|
1368
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1369
|
-
assert_eq!(id, "185");
|
|
1370
|
-
assert_eq!(extra, vec!["-R", "rtk-ai/rtk"]);
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
#[test]
|
|
1374
|
-
fn test_extract_identifier_with_long_repo_flag() {
|
|
1375
|
-
let args: Vec<String> = vec!["42".into(), "--repo".into(), "owner/repo".into()];
|
|
1376
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1377
|
-
assert_eq!(id, "42");
|
|
1378
|
-
assert_eq!(extra, vec!["--repo", "owner/repo"]);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
#[test]
|
|
1382
|
-
fn test_extract_identifier_empty() {
|
|
1383
|
-
let args: Vec<String> = vec![];
|
|
1384
|
-
assert!(extract_identifier_and_extra_args(&args).is_none());
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
#[test]
|
|
1388
|
-
fn test_extract_identifier_only_flags() {
|
|
1389
|
-
// No positional identifier, only flags
|
|
1390
|
-
let args: Vec<String> = vec!["-R".into(), "rtk-ai/rtk".into()];
|
|
1391
|
-
assert!(extract_identifier_and_extra_args(&args).is_none());
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
#[test]
|
|
1395
|
-
fn test_extract_identifier_with_web_flag() {
|
|
1396
|
-
let args: Vec<String> = vec!["123".into(), "--web".into()];
|
|
1397
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1398
|
-
assert_eq!(id, "123");
|
|
1399
|
-
assert_eq!(extra, vec!["--web"]);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
#[test]
|
|
1403
|
-
fn test_run_view_passthrough_log_failed() {
|
|
1404
|
-
assert!(should_passthrough_run_view(&["--log-failed".into()]));
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
#[test]
|
|
1408
|
-
fn test_run_view_passthrough_log() {
|
|
1409
|
-
assert!(should_passthrough_run_view(&["--log".into()]));
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
#[test]
|
|
1413
|
-
fn test_run_view_passthrough_json() {
|
|
1414
|
-
assert!(should_passthrough_run_view(&[
|
|
1415
|
-
"--json".into(),
|
|
1416
|
-
"jobs".into()
|
|
1417
|
-
]));
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
#[test]
|
|
1421
|
-
fn test_run_view_no_passthrough_empty() {
|
|
1422
|
-
assert!(!should_passthrough_run_view(&[]));
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
#[test]
|
|
1426
|
-
fn test_run_view_no_passthrough_other_flags() {
|
|
1427
|
-
assert!(!should_passthrough_run_view(&["--web".into()]));
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
#[test]
|
|
1431
|
-
fn test_extract_identifier_with_job_flag_after() {
|
|
1432
|
-
// gh run view 12345 --job 67890
|
|
1433
|
-
let args: Vec<String> = vec!["12345".into(), "--job".into(), "67890".into()];
|
|
1434
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1435
|
-
assert_eq!(id, "12345");
|
|
1436
|
-
assert_eq!(extra, vec!["--job", "67890"]);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
#[test]
|
|
1440
|
-
fn test_extract_identifier_with_job_flag_before() {
|
|
1441
|
-
// gh run view --job 67890 12345
|
|
1442
|
-
let args: Vec<String> = vec!["--job".into(), "67890".into(), "12345".into()];
|
|
1443
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1444
|
-
assert_eq!(id, "12345");
|
|
1445
|
-
assert_eq!(extra, vec!["--job", "67890"]);
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
#[test]
|
|
1449
|
-
fn test_extract_identifier_with_job_and_log_failed() {
|
|
1450
|
-
// gh run view --log-failed --job 67890 12345
|
|
1451
|
-
let args: Vec<String> = vec![
|
|
1452
|
-
"--log-failed".into(),
|
|
1453
|
-
"--job".into(),
|
|
1454
|
-
"67890".into(),
|
|
1455
|
-
"12345".into(),
|
|
1456
|
-
];
|
|
1457
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1458
|
-
assert_eq!(id, "12345");
|
|
1459
|
-
assert_eq!(extra, vec!["--log-failed", "--job", "67890"]);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
#[test]
|
|
1463
|
-
fn test_extract_identifier_with_attempt_flag() {
|
|
1464
|
-
// gh run view 12345 --attempt 3
|
|
1465
|
-
let args: Vec<String> = vec!["12345".into(), "--attempt".into(), "3".into()];
|
|
1466
|
-
let (id, extra) = extract_identifier_and_extra_args(&args).unwrap();
|
|
1467
|
-
assert_eq!(id, "12345");
|
|
1468
|
-
assert_eq!(extra, vec!["--attempt", "3"]);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
// --- should_passthrough_pr_view tests ---
|
|
1472
|
-
|
|
1473
|
-
#[test]
|
|
1474
|
-
fn test_should_passthrough_pr_view_json() {
|
|
1475
|
-
assert!(should_passthrough_pr_view(&[
|
|
1476
|
-
"--json".into(),
|
|
1477
|
-
"body,comments".into()
|
|
1478
|
-
]));
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
#[test]
|
|
1482
|
-
fn test_should_passthrough_pr_view_jq() {
|
|
1483
|
-
assert!(should_passthrough_pr_view(&["--jq".into(), ".body".into()]));
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
#[test]
|
|
1487
|
-
fn test_should_passthrough_pr_view_web() {
|
|
1488
|
-
assert!(should_passthrough_pr_view(&["--web".into()]));
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
#[test]
|
|
1492
|
-
fn test_should_passthrough_pr_view_default() {
|
|
1493
|
-
assert!(!should_passthrough_pr_view(&[]));
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
#[test]
|
|
1497
|
-
fn test_should_passthrough_pr_view_other_flags() {
|
|
1498
|
-
assert!(!should_passthrough_pr_view(&["--comments".into()]));
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
// --- filter_markdown_body tests ---
|
|
1502
|
-
|
|
1503
|
-
#[test]
|
|
1504
|
-
fn test_filter_markdown_body_html_comment_single_line() {
|
|
1505
|
-
let input = "Hello\n<!-- this is a comment -->\nWorld";
|
|
1506
|
-
let result = filter_markdown_body(input);
|
|
1507
|
-
assert!(!result.contains("<!--"));
|
|
1508
|
-
assert!(result.contains("Hello"));
|
|
1509
|
-
assert!(result.contains("World"));
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
#[test]
|
|
1513
|
-
fn test_filter_markdown_body_html_comment_multiline() {
|
|
1514
|
-
let input = "Before\n<!--\nmultiline\ncomment\n-->\nAfter";
|
|
1515
|
-
let result = filter_markdown_body(input);
|
|
1516
|
-
assert!(!result.contains("<!--"));
|
|
1517
|
-
assert!(!result.contains("multiline"));
|
|
1518
|
-
assert!(result.contains("Before"));
|
|
1519
|
-
assert!(result.contains("After"));
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
#[test]
|
|
1523
|
-
fn test_filter_markdown_body_badge_lines() {
|
|
1524
|
-
let input = "# Title\n[](https://github.com/actions)\nSome text";
|
|
1525
|
-
let result = filter_markdown_body(input);
|
|
1526
|
-
assert!(!result.contains("shields.io"));
|
|
1527
|
-
assert!(result.contains("# Title"));
|
|
1528
|
-
assert!(result.contains("Some text"));
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
#[test]
|
|
1532
|
-
fn test_filter_markdown_body_image_only_lines() {
|
|
1533
|
-
let input = "# Title\n\nSome text";
|
|
1534
|
-
let result = filter_markdown_body(input);
|
|
1535
|
-
assert!(!result.contains("![screenshot]"));
|
|
1536
|
-
assert!(result.contains("# Title"));
|
|
1537
|
-
assert!(result.contains("Some text"));
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
#[test]
|
|
1541
|
-
fn test_filter_markdown_body_horizontal_rules() {
|
|
1542
|
-
let input = "Section 1\n---\nSection 2\n***\nSection 3\n___\nEnd";
|
|
1543
|
-
let result = filter_markdown_body(input);
|
|
1544
|
-
assert!(!result.contains("---"));
|
|
1545
|
-
assert!(!result.contains("***"));
|
|
1546
|
-
assert!(!result.contains("___"));
|
|
1547
|
-
assert!(result.contains("Section 1"));
|
|
1548
|
-
assert!(result.contains("Section 2"));
|
|
1549
|
-
assert!(result.contains("Section 3"));
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
#[test]
|
|
1553
|
-
fn test_filter_markdown_body_blank_lines_collapse() {
|
|
1554
|
-
let input = "Line 1\n\n\n\n\nLine 2";
|
|
1555
|
-
let result = filter_markdown_body(input);
|
|
1556
|
-
// Should collapse to at most one blank line (2 newlines)
|
|
1557
|
-
assert!(!result.contains("\n\n\n"));
|
|
1558
|
-
assert!(result.contains("Line 1"));
|
|
1559
|
-
assert!(result.contains("Line 2"));
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
#[test]
|
|
1563
|
-
fn test_filter_markdown_body_code_block_preserved() {
|
|
1564
|
-
let input = "Text before\n```python\n<!-- not a comment -->\n\n---\n```\nText after";
|
|
1565
|
-
let result = filter_markdown_body(input);
|
|
1566
|
-
// Content inside code block should be preserved
|
|
1567
|
-
assert!(result.contains("<!-- not a comment -->"));
|
|
1568
|
-
assert!(result.contains(""));
|
|
1569
|
-
assert!(result.contains("---"));
|
|
1570
|
-
assert!(result.contains("Text before"));
|
|
1571
|
-
assert!(result.contains("Text after"));
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
#[test]
|
|
1575
|
-
fn test_filter_markdown_body_empty() {
|
|
1576
|
-
assert_eq!(filter_markdown_body(""), "");
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
#[test]
|
|
1580
|
-
fn test_filter_markdown_body_meaningful_content_preserved() {
|
|
1581
|
-
let input = "## Summary\n- Item 1\n- Item 2\n\n[Link](https://example.com)\n\n| Col1 | Col2 |\n| --- | --- |\n| a | b |";
|
|
1582
|
-
let result = filter_markdown_body(input);
|
|
1583
|
-
assert!(result.contains("## Summary"));
|
|
1584
|
-
assert!(result.contains("- Item 1"));
|
|
1585
|
-
assert!(result.contains("- Item 2"));
|
|
1586
|
-
assert!(result.contains("[Link](https://example.com)"));
|
|
1587
|
-
assert!(result.contains("| Col1 | Col2 |"));
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
#[test]
|
|
1591
|
-
fn test_filter_markdown_body_token_savings() {
|
|
1592
|
-
// Realistic PR body with noise
|
|
1593
|
-
let input = r#"<!-- This PR template is auto-generated -->
|
|
1594
|
-
<!-- Please fill in the following sections -->
|
|
1595
|
-
|
|
1596
|
-
## Summary
|
|
1597
|
-
|
|
1598
|
-
Added smart markdown filtering for gh issue/pr view commands.
|
|
1599
|
-
|
|
1600
|
-
[](https://github.com/rtk-ai/rtk/actions)
|
|
1601
|
-
[](https://codecov.io/gh/rtk-ai/rtk)
|
|
1602
|
-
|
|
1603
|
-

|
|
1604
|
-
|
|
1605
|
-
---
|
|
1606
|
-
|
|
1607
|
-
## Changes
|
|
1608
|
-
|
|
1609
|
-
- Filter HTML comments
|
|
1610
|
-
- Filter badge lines
|
|
1611
|
-
- Filter image-only lines
|
|
1612
|
-
- Collapse blank lines
|
|
1613
|
-
|
|
1614
|
-
***
|
|
1615
|
-
|
|
1616
|
-
## Test Plan
|
|
1617
|
-
|
|
1618
|
-
- [x] Unit tests added
|
|
1619
|
-
- [x] Snapshot tests pass
|
|
1620
|
-
- [ ] Manual testing
|
|
1621
|
-
|
|
1622
|
-
___
|
|
1623
|
-
|
|
1624
|
-
<!-- Do not edit below this line -->
|
|
1625
|
-
<!-- Auto-generated footer -->"#;
|
|
1626
|
-
|
|
1627
|
-
let result = filter_markdown_body(input);
|
|
1628
|
-
|
|
1629
|
-
fn count_tokens(text: &str) -> usize {
|
|
1630
|
-
text.split_whitespace().count()
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
let input_tokens = count_tokens(input);
|
|
1634
|
-
let output_tokens = count_tokens(&result);
|
|
1635
|
-
let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0);
|
|
1636
|
-
|
|
1637
|
-
assert!(
|
|
1638
|
-
savings >= 30.0,
|
|
1639
|
-
"Expected ≥30% savings, got {:.1}% (input: {} tokens, output: {} tokens)",
|
|
1640
|
-
savings,
|
|
1641
|
-
input_tokens,
|
|
1642
|
-
output_tokens
|
|
1643
|
-
);
|
|
1644
|
-
|
|
1645
|
-
// Verify meaningful content preserved
|
|
1646
|
-
assert!(result.contains("## Summary"));
|
|
1647
|
-
assert!(result.contains("## Changes"));
|
|
1648
|
-
assert!(result.contains("## Test Plan"));
|
|
1649
|
-
assert!(result.contains("Filter HTML comments"));
|
|
1650
|
-
}
|
|
1651
|
-
}
|