• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 24128084162

08 Apr 2026 09:24AM UTC coverage: 62.329% (-2.0%) from 64.307%
24128084162

Pull #521

github

web-flow
Merge 7c1ff4817 into 9ea8e05a0
Pull Request #521: feat(plugin): Add command plugin system (RFD 072)

118 of 1184 new or added lines in 15 files covered. (9.97%)

5 existing lines in 2 files now uncovered.

20176 of 32370 relevant lines covered (62.33%)

256.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/plugins/command/path/src/main.rs
1
//! `jp-path`: print JP directory paths.
2
//!
3
//! A command plugin that prints well-known JP directory paths for use in
4
//! shell scripts and automation. All paths come from the host's `init`
5
//! message, so this plugin has no platform-specific logic.
6
//!
7
//! See: `docs/rfd/D17-command-plugin-system.md`
8

9
use std::io::{self, BufRead, BufReader, IsTerminal as _, Write};
10

11
use jp_plugin::message::{
12
    DescribeResponse, ExitMessage, HostToPlugin, InitMessage, PluginToHost, PrintMessage,
13
};
14

15
const HELP_TEXT: &str = "\
16
Print JP directory paths.
17

18
Usage: jp path <COMMAND> [OPTIONS]
19

20
Commands:
21
  user-local       User-local data directory
22
  user-config      User-global config directory
23
  workspace        Workspace storage directory (.jp)
24
  user-workspace   User-local workspace storage directory
25

26
Options for user-local:
27
  --plugins=command  Print the command plugin install directory";
28

NEW
29
fn main() {
×
NEW
30
    if io::stdin().is_terminal() {
×
NEW
31
        let mut err = io::stderr().lock();
×
NEW
32
        drop(writeln!(err, "{HELP_TEXT}"));
×
NEW
33
        drop(writeln!(err));
×
NEW
34
        drop(writeln!(
×
NEW
35
            err,
×
36
            "Note: this binary is a JP plugin. Run it via `jp path`."
37
        ));
NEW
38
        std::process::exit(0);
×
NEW
39
    }
×
40

NEW
41
    let stdin = BufReader::new(io::stdin());
×
NEW
42
    let stdout = io::stdout();
×
43

NEW
44
    let code = match run(stdin, stdout) {
×
NEW
45
        Ok(()) => 0,
×
NEW
46
        Err(e) => {
×
NEW
47
            let mut err = io::stderr().lock();
×
NEW
48
            drop(writeln!(err, "Fatal: {e}"));
×
NEW
49
            1
×
50
        }
51
    };
52

NEW
53
    std::process::exit(code);
×
54
}
55

NEW
56
fn run(mut stdin: impl BufRead, mut stdout: impl Write) -> Result<(), String> {
×
NEW
57
    let first_msg = read_message(&mut stdin)?;
×
58

NEW
59
    match first_msg {
×
NEW
60
        HostToPlugin::Describe => send_describe(&mut stdout),
×
NEW
61
        HostToPlugin::Init(init) => {
×
NEW
62
            send(&mut stdout, &PluginToHost::Ready)?;
×
NEW
63
            handle_command(&init, &mut stdout)
×
64
        }
NEW
65
        other => Err(format!("expected init or describe, got: {other:?}")),
×
66
    }
NEW
67
}
×
68

NEW
69
fn handle_command(init: &InitMessage, stdout: &mut impl Write) -> Result<(), String> {
×
NEW
70
    let args = &init.args;
×
NEW
71
    let subcommand = args.first().map(String::as_str);
×
72

NEW
73
    let result = match subcommand {
×
NEW
74
        Some("user-local") => handle_user_local(init, args),
×
NEW
75
        Some("user-config") => handle_user_config(init),
×
NEW
76
        Some("workspace") => Ok(init.workspace.storage.to_string()),
×
NEW
77
        Some("user-workspace") => handle_user_workspace(init),
×
NEW
78
        Some(other) => Err(format!("unknown subcommand: {other}\n\n{HELP_TEXT}")),
×
NEW
79
        None => Err(format!("missing subcommand\n\n{HELP_TEXT}")),
×
80
    };
81

NEW
82
    match result {
×
NEW
83
        Ok(path) => {
×
NEW
84
            send(
×
NEW
85
                stdout,
×
NEW
86
                &PluginToHost::Print(PrintMessage {
×
NEW
87
                    text: format!("{path}\n"),
×
NEW
88
                    channel: "content".into(),
×
NEW
89
                    format: "plain".into(),
×
NEW
90
                    language: None,
×
NEW
91
                }),
×
NEW
92
            )?;
×
NEW
93
            send_exit(stdout, 0, None)
×
94
        }
NEW
95
        Err(msg) => send_exit(stdout, 1, Some(&msg)),
×
96
    }
NEW
97
}
×
98

NEW
99
fn handle_user_local(init: &InitMessage, args: &[String]) -> Result<String, String> {
×
NEW
100
    let base = init
×
NEW
101
        .paths
×
NEW
102
        .user_data
×
NEW
103
        .as_ref()
×
NEW
104
        .ok_or("host did not provide user data path")?;
×
105

106
    // Check for --plugins=command flag.
NEW
107
    let plugins_value = args.iter().find_map(|a| a.strip_prefix("--plugins="));
×
108

NEW
109
    match plugins_value {
×
NEW
110
        Some("command") => Ok(format!("{base}/plugins/command")),
×
NEW
111
        Some(other) => Err(format!("unknown --plugins value: {other}")),
×
NEW
112
        None => Ok(base.to_string()),
×
113
    }
NEW
114
}
×
115

NEW
116
fn handle_user_config(init: &InitMessage) -> Result<String, String> {
×
NEW
117
    init.paths
×
NEW
118
        .user_config
×
NEW
119
        .as_ref()
×
NEW
120
        .map(ToString::to_string)
×
NEW
121
        .ok_or_else(|| "host did not provide user config path".to_owned())
×
NEW
122
}
×
123

NEW
124
fn handle_user_workspace(init: &InitMessage) -> Result<String, String> {
×
NEW
125
    init.paths
×
NEW
126
        .user_workspace
×
NEW
127
        .as_ref()
×
NEW
128
        .map(ToString::to_string)
×
NEW
129
        .ok_or_else(|| "no user-workspace storage configured for this workspace".to_owned())
×
NEW
130
}
×
131

NEW
132
fn send_describe(stdout: &mut impl Write) -> Result<(), String> {
×
NEW
133
    send(
×
NEW
134
        stdout,
×
NEW
135
        &PluginToHost::Describe(DescribeResponse {
×
NEW
136
            name: "path".to_owned(),
×
NEW
137
            version: env!("CARGO_PKG_VERSION").to_owned(),
×
NEW
138
            description: "Print JP directory paths".to_owned(),
×
NEW
139
            command: vec!["path".to_owned()],
×
NEW
140
            author: Some("Jean Mertz <git@jeanmertz.com>".to_owned()),
×
NEW
141
            help: Some(HELP_TEXT.to_owned()),
×
NEW
142
            repository: Some("https://github.com/dcdpr/jp".to_owned()),
×
NEW
143
        }),
×
144
    )
NEW
145
}
×
146

NEW
147
fn send_exit(stdout: &mut impl Write, code: u8, reason: Option<&str>) -> Result<(), String> {
×
NEW
148
    send(
×
NEW
149
        stdout,
×
NEW
150
        &PluginToHost::Exit(ExitMessage {
×
NEW
151
            code,
×
NEW
152
            reason: reason.map(String::from),
×
NEW
153
        }),
×
154
    )
NEW
155
}
×
156

NEW
157
fn read_message(stdin: &mut impl BufRead) -> Result<HostToPlugin, String> {
×
NEW
158
    let mut line = String::new();
×
NEW
159
    stdin
×
NEW
160
        .read_line(&mut line)
×
NEW
161
        .map_err(|e| format!("failed to read from host: {e}"))?;
×
162

NEW
163
    serde_json::from_str(line.trim()).map_err(|e| format!("invalid host message: {e}"))
×
NEW
164
}
×
165

NEW
166
fn send(stdout: &mut impl Write, msg: &PluginToHost) -> Result<(), String> {
×
NEW
167
    let json = serde_json::to_string(msg).map_err(|e| format!("serialize error: {e}"))?;
×
NEW
168
    writeln!(stdout, "{json}").map_err(|e| format!("write error: {e}"))?;
×
NEW
169
    stdout.flush().map_err(|e| format!("flush error: {e}"))
×
NEW
170
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc