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

dcdpr / jp / 26664039893

29 May 2026 09:50PM UTC coverage: 66.375% (+0.003%) from 66.372%
26664039893

push

github

web-flow
chore: reformat all markdown files using `comfort` (#699)

Signed-off-by: Jean Mertz <git@jeanmertz.com>

32028 of 48253 relevant lines covered (66.38%)

269.79 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 shell
4
//! scripts and automation.
5
//! All paths come from the host's `init` message, so this plugin has no
6
//! platform-specific logic.
7
//!
8
//! See: `docs/rfd/072-command-plugin-system.md`
9

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc