Skip to main content

supercode_harness/
teams.rs

1//! Where supercode-teams lives on this box, and the service unit that keeps
2//! its machine daemon up (`docs/plans/teams-server.md` §11).
3//!
4//! supercode does not implement teams; the `sdk/teams` package does. This
5//! module holds the two facts the Rust CLI needs about it:
6//!
7//! * **where its Node entry is** — [`teams_entry`], resolved exactly the way
8//!   [`crate::orchestrator::daemon_entry`] resolves the orchestrator's:
9//!   `SUPERCODE_TEAMS_ENTRY` first, then the checkout the running binary sits
10//!   in, then the checkout it was built from, then the globally installed
11//!   `@volter/supercode-teams` package (`npm root -g`).
12//! * **what a service unit for its node would say** — [`service_unit`] renders
13//!   the launchd plist / systemd unit / Windows Scheduled Task that runs
14//!   `node <entry> machine start`, written under `<home>/service/`;
15//!   [`install_service`] and [`uninstall_service`] drive `launchctl` /
16//!   `systemctl --user` / `schtasks` over it.
17//!
18//! Everything else about teams — its host key, log, contexts, enrollments — is the Node
19//! package's own state, written by its own CLI. There is no second writer of
20//! that home in this binary.
21
22use std::path::{Path, PathBuf};
23
24use crate::orchestrator::{absolute_program, resolve_program, ServiceState, ServiceUnit};
25
26/// The teams CLI entry inside the `sdk/teams` package.
27pub const TEAMS_ENTRY: &str = "bin/teams.mjs";
28
29/// The npm name the `sdk/teams` package is published under.
30pub const TEAMS_PACKAGE: &str = "@volter/supercode-teams";
31
32/// Directory the rendered service unit is written into, relative to the home.
33pub const SERVICE_DIR: &str = "service";
34
35/// launchd label / systemd unit name for this machine's teams daemon.
36pub const SERVICE_NAME: &str = "dev.volter.supercode-teams-machine";
37
38/// Stable, context-scoped label for one workspace connector service.
39pub fn connector_service_name(server_id: &str, team_id: &str, context: &str) -> String {
40    // FNV-1a is sufficient here: this is a stable filesystem/service label,
41    // not an authorization decision or secret digest.
42    let mut hash = 0xcbf29ce484222325_u64;
43    for byte in [server_id, team_id, context].join("\0").bytes() {
44        hash ^= u64::from(byte);
45        hash = hash.wrapping_mul(0x100000001b3);
46    }
47    format!("dev.volter.supercode-teams-connector-{hash:016x}")
48}
49
50fn plist_text(value: &str) -> String {
51    value
52        .replace('&', "&amp;")
53        .replace('<', "&lt;")
54        .replace('>', "&gt;")
55}
56
57fn service_text(value: &str) -> Result<&str, TeamsError> {
58    if value.chars().any(char::is_control) {
59        return Err(TeamsError::Service {
60            action: "render",
61            detail: "service parameters cannot contain control characters".into(),
62        });
63    }
64    Ok(value)
65}
66
67fn systemd_arg(value: &str) -> String {
68    format!(
69        "\"{}\"",
70        value
71            .replace('\\', "\\\\")
72            .replace('"', "\\\"")
73            .replace('%', "%%")
74            .replace('$', "$$")
75    )
76}
77
78/// Render the persistent foreground connector command for one saved context.
79pub fn connector_service_unit(
80    teams_home: &Path,
81    supercode_home: &Path,
82    entry: &Path,
83    node: &str,
84    supercode: &Path,
85    context: &str,
86    cwd: &Path,
87    server_id: &str,
88    team_id: &str,
89) -> Result<ServiceUnit, TeamsError> {
90    let teams_home_text = teams_home.display().to_string();
91    let supercode_home_text = supercode_home.display().to_string();
92    let entry_text = entry.display().to_string();
93    let supercode_text = supercode.display().to_string();
94    let workspace_text = cwd.display().to_string();
95    for value in [
96        teams_home_text.as_str(),
97        supercode_home_text.as_str(),
98        entry_text.as_str(),
99        node,
100        supercode_text.as_str(),
101        context,
102        workspace_text.as_str(),
103        server_id,
104        team_id,
105    ] {
106        service_text(value)?;
107    }
108    let label = connector_service_name(server_id, team_id, context);
109    let path = teams_home
110        .join(SERVICE_DIR)
111        .join(format!("{label}.{}", connector_unit_suffix()));
112    let node = absolute_program(node);
113    let entry = entry_text;
114    let workspace = workspace_text;
115    let home = supercode_home_text;
116    let supercode = supercode_text;
117    if cfg!(windows) {
118        let log_path = teams_home.join(SERVICE_DIR).join(format!("{label}.log"));
119        return windows_task(
120            &path,
121            &label,
122            &format!("supercode Teams connector ({context})"),
123            &service_env_path(teams_home, &label),
124            &[
125                ("SUPERCODE_HOME", home.as_str()),
126                ("SUPERCODE_BIN", supercode.as_str()),
127                ("SUPERCODE_TEAMS_LOG", &log_path.display().to_string()),
128            ],
129            &node,
130            &[
131                entry.as_str(),
132                "teams",
133                "connect",
134                "--foreground",
135                "--context",
136                context,
137                "--cwd",
138                workspace.as_str(),
139            ],
140            &workspace,
141        );
142    }
143    if cfg!(target_os = "macos") {
144        let node = plist_text(&node);
145        let entry = plist_text(&entry);
146        let workspace = plist_text(&workspace);
147        let home = plist_text(&home);
148        let supercode = plist_text(&supercode);
149        let context = plist_text(context);
150        let search_path = plist_text(&service_path());
151        let text = format!(
152            r#"<?xml version="1.0" encoding="UTF-8"?>
153<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
154<plist version="1.0"><dict>
155  <key>Label</key><string>{label}</string>
156  <key>ProgramArguments</key><array><string>{node}</string><string>{entry}</string><string>teams</string><string>connect</string><string>--context</string><string>{context}</string><string>--cwd</string><string>{workspace}</string></array>
157  <key>EnvironmentVariables</key><dict><key>SUPERCODE_HOME</key><string>{home}</string><key>SUPERCODE_BIN</key><string>{supercode}</string><key>PATH</key><string>{search_path}</string></dict>
158  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
159  <key>StandardOutPath</key><string>{}/service/{label}.out.log</string>
160  <key>StandardErrorPath</key><string>{}/service/{label}.err.log</string>
161</dict></plist>
162"#,
163            plist_text(&teams_home_text),
164            plist_text(&teams_home_text)
165        );
166        Ok(ServiceUnit {
167            kind: "launchd",
168            path: path.clone(),
169            text,
170            install_command: format!("launchctl bootstrap gui/$(id -u) {}", path.display()),
171            files: Vec::new(),
172        })
173    } else {
174        let environment_home = systemd_arg(&format!("SUPERCODE_HOME={home}"));
175        let environment_bin = systemd_arg(&format!("SUPERCODE_BIN={supercode}"));
176        let environment_path = systemd_arg(&format!("PATH={}", service_path()));
177        let node = systemd_arg(&node);
178        let entry = systemd_arg(&entry);
179        let workspace = systemd_arg(&workspace);
180        let context_description = context.replace('%', "%%").replace('$', "$$");
181        let context = systemd_arg(context);
182        let text = format!("[Unit]\nDescription=supercode Teams connector ({context_description})\nAfter=network.target\n\n[Service]\nEnvironment={environment_home}\nEnvironment={environment_bin}\nEnvironment={environment_path}\nExecStart={node} {entry} teams connect --context {context} --cwd {workspace}\nRestart=on-failure\nKillSignal=SIGTERM\n\n[Install]\nWantedBy=default.target\n");
183        Ok(ServiceUnit {
184            kind: "systemd",
185            path: path.clone(),
186            text,
187            install_command: format!(
188                "systemctl --user link {} && systemctl --user enable --now {label}",
189                path.display()
190            ),
191            files: Vec::new(),
192        })
193    }
194}
195
196/// The connector unit's file suffix on this platform.
197fn connector_unit_suffix() -> &'static str {
198    if cfg!(windows) {
199        "xml"
200    } else if cfg!(target_os = "macos") {
201        "plist"
202    } else {
203        "service"
204    }
205}
206
207/// The environment file a Windows service task hands to node (`--env-file`).
208fn service_env_path(teams_home: &Path, label: &str) -> PathBuf {
209    teams_home.join(SERVICE_DIR).join(format!("{label}.env"))
210}
211
212/// Render a per-user Scheduled Task that keeps `node --env-file=<env_path> <node_args…>` running, written to `path`
213/// with its environment file beside it. Shared by the connector and the machine daemon.
214#[allow(clippy::too_many_arguments)]
215fn windows_task(
216    path: &Path,
217    label: &str,
218    description: &str,
219    env_path: &Path,
220    env: &[(&str, &str)],
221    node: &str,
222    node_args: &[&str],
223    working_directory: &str,
224) -> Result<ServiceUnit, TeamsError> {
225    // A scheduled task sets no environment and keeps no output, so node reads both from a file beside the
226    // task (`--env-file`); the user's own PATH is the task's. `conhost --headless` runs it without a window,
227    // and hides node's exit code too, so restart-on-failure never sees one fail: a trigger every minute
228    // starts the service again instead, and while it runs the task's IgnoreNew makes that tick a no-op.
229    // The trigger's fixed past start keeps the rendered unit the same on every install.
230    let env_text = env
231        .iter()
232        .map(|(key, value)| env_file_value(value).map(|value| format!("{key}={value}\n")))
233        .collect::<Result<String, _>>()?;
234    let env_flag = format!("--env-file={}", env_path.display());
235    let mut arguments = vec![node, env_flag.as_str()];
236    arguments.extend_from_slice(node_args);
237    // Task Scheduler expands `%NAME%` in a task's arguments, and there is no escape for it.
238    for value in arguments.iter().chain([&working_directory]) {
239        if value.contains('%') {
240            return Err(TeamsError::Service {
241                action: "render",
242                detail: format!("a Windows task cannot carry a path containing `%`: {value}"),
243            });
244        }
245    }
246    let arguments = arguments
247        .iter()
248        .map(|argument| windows_arg(argument))
249        .collect::<Vec<_>>()
250        .join(" ");
251    let user = windows_user();
252    let conhost = Path::new(&std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".into()))
253        .join(r"System32\conhost.exe")
254        .display()
255        .to_string();
256    let text = format!(
257        r#"<?xml version="1.0" encoding="UTF-16"?>
258<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
259  <RegistrationInfo><Description>{}</Description></RegistrationInfo>
260  <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>{}</UserId></LogonTrigger><TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary><Enabled>true</Enabled><Repetition><Interval>PT1M</Interval><StopAtDurationEnd>false</StopAtDurationEnd></Repetition></TimeTrigger></Triggers>
261  <Principals><Principal id="Author"><UserId>{}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
262  <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure><StartWhenAvailable>true</StartWhenAvailable></Settings>
263  <Actions Context="Author"><Exec><Command>{}</Command><Arguments>--headless {}</Arguments><WorkingDirectory>{}</WorkingDirectory></Exec></Actions>
264</Task>
265"#,
266        xml_text(description),
267        xml_text(&user),
268        xml_text(&user),
269        xml_text(&conhost),
270        xml_text(&arguments),
271        xml_text(working_directory),
272    );
273    Ok(ServiceUnit {
274        kind: "schtasks",
275        path: path.to_path_buf(),
276        text,
277        install_command: format!("schtasks /Create /TN {label} /XML {} /F", path.display()),
278        files: vec![(env_path.to_path_buf(), env_text)],
279    })
280}
281
282/// Text inside a Task Scheduler XML element or attribute.
283fn xml_text(value: &str) -> String {
284    plist_text(value).replace('"', "&quot;")
285}
286
287/// One argument of a Windows command line, quoted so the C runtime (node.exe's) splits it back out whole:
288/// backslashes are literal except before a quote, where they and the quote are escaped.
289fn windows_arg(value: &str) -> String {
290    if !value.is_empty() && !value.contains([' ', '\t', '"']) {
291        return value.to_string();
292    }
293    let mut quoted = String::from("\"");
294    let mut backslashes = 0;
295    for character in value.chars() {
296        match character {
297            '\\' => backslashes += 1,
298            '"' => {
299                quoted.push_str(&"\\".repeat(backslashes * 2 + 1));
300                quoted.push('"');
301                backslashes = 0;
302            }
303            other => {
304                quoted.push_str(&"\\".repeat(backslashes));
305                quoted.push(other);
306                backslashes = 0;
307            }
308        }
309    }
310    quoted.push_str(&"\\".repeat(backslashes * 2));
311    quoted.push('"');
312    quoted
313}
314
315/// A value in a node `--env-file`, quoted with a quote character the value does not contain. Single and backtick
316/// quotes are literal; double quotes would turn a path's `\n` into a newline, so they are the last choice.
317fn env_file_value(value: &str) -> Result<String, TeamsError> {
318    ['\'', '`']
319        .into_iter()
320        .find(|quote| !value.contains(*quote))
321        .map(|quote| format!("{quote}{value}{quote}"))
322        .or_else(|| (!value.contains(['"', '\\'])).then(|| format!("\"{value}\"")))
323        .ok_or_else(|| TeamsError::Service {
324            action: "render",
325            detail: format!("cannot write `{value}` into a service's environment file"),
326        })
327}
328
329/// The Windows account a task runs as: `DOMAIN\user`, the one installing it.
330fn windows_user() -> String {
331    let user = std::env::var("USERNAME").unwrap_or_default();
332    match std::env::var("USERDOMAIN") {
333        Ok(domain) if !domain.is_empty() => format!("{domain}\\{user}"),
334        _ => user,
335    }
336}
337
338/// Why a teams verb could not do its work.
339#[derive(Debug, thiserror::Error)]
340pub enum TeamsError {
341    /// The Node teams entry could not be located.
342    #[error("no teams entry found (looked for `sdk/teams/{TEAMS_ENTRY}` under: {searched}); install it with `npm install -g {TEAMS_PACKAGE}`")]
343    NoEntry {
344        /// The candidate paths that were searched, joined.
345        searched: String,
346    },
347    /// A service manager refused, or there is none on this platform.
348    #[error("teams service: {action} failed: {detail}")]
349    Service {
350        /// What was attempted (`install`, `uninstall`).
351        action: &'static str,
352        /// What the service manager (or this module) said about it.
353        detail: String,
354    },
355    /// A file under the teams home could not be written or removed.
356    #[error("teams file `{}`: {source}", path.display())]
357    File {
358        /// The path involved.
359        path: PathBuf,
360        /// The underlying I/O failure.
361        source: std::io::Error,
362    },
363}
364
365/// The search path a service runs with: the installing shell's own, so a
366/// service finds the same `tmux`, `node` and harness CLIs its installer did
367/// (a launchd or systemd default path has none of them).
368fn service_path() -> String {
369    std::env::var("PATH")
370        .ok()
371        .filter(|path| !path.trim().is_empty())
372        .unwrap_or_else(|| "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin".into())
373}
374
375/// The teams home: `SUPERCODE_TEAMS_HOME`, else `<SUPERCODE_HOME>/teams`.
376///
377/// The same precedence `sdk/volter-teams/home.mjs` uses, so a unit installed from
378/// here serves the home the Node CLI reads.
379pub fn teams_home() -> PathBuf {
380    if let Ok(home) = std::env::var("SUPERCODE_TEAMS_HOME") {
381        if !home.is_empty() {
382            return PathBuf::from(home);
383        }
384    }
385    crate::agent::global_instructions_dir().join("teams")
386}
387
388/// Locate the Node teams entry (`sdk/teams/bin/teams.mjs`).
389///
390/// Candidates, in order: `SUPERCODE_TEAMS_ENTRY` (an explicit override, which
391/// is also how a test points at a fake), the repo checkout the running binary
392/// sits in, the workspace this crate was built from,
393/// and the globally installed npm package — an installed binary has no
394/// checkout, so `npm install -g @volter/supercode-teams` is how a
395/// Machine gets its node. The current directory is never a candidate: the
396/// code a binary runs does not change with where it is run.
397pub fn teams_entry() -> Result<PathBuf, TeamsError> {
398    let mut searched = Vec::new();
399    if let Some(explicit) = std::env::var_os("SUPERCODE_TEAMS_ENTRY") {
400        let path = PathBuf::from(explicit);
401        if path.is_file() {
402            return Ok(path);
403        }
404        searched.push(path.display().to_string());
405    }
406    let mut roots: Vec<PathBuf> = Vec::new();
407    if let Ok(exe) = std::env::current_exe() {
408        // target/<profile>/supercode → the workspace root is two levels up.
409        roots.extend(exe.ancestors().skip(1).take(4).map(Path::to_path_buf));
410    }
411    // A locally built binary's target directory can live anywhere (a shared
412    // cargo build dir, another volume), so the checkout it was built from is
413    // the last candidate. On an installed binary this path simply does not
414    // exist and is skipped like any other miss.
415    if let Some(workspace) = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2) {
416        roots.push(workspace.to_path_buf());
417    }
418    for root in roots {
419        let candidate = root.join("sdk/teams").join(TEAMS_ENTRY);
420        if candidate.is_file() {
421            return Ok(candidate);
422        }
423        searched.push(candidate.display().to_string());
424    }
425    // Installed from npm, this binary sits inside the global node_modules that
426    // also holds the Teams package, so that directory is found from the
427    // binary's own path first (`npm root -g` masks path segments it takes for
428    // secrets, a UUID among them).
429    if let Ok(exe) = std::env::current_exe() {
430        for modules in exe
431            .ancestors()
432            .filter(|dir| dir.file_name().is_some_and(|name| name == "node_modules"))
433        {
434            let candidate = modules.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
435            if candidate.is_file() {
436                return Ok(candidate);
437            }
438            searched.push(candidate.display().to_string());
439        }
440    }
441    if let Some(global) = global_npm_root() {
442        let candidate = global.join(TEAMS_PACKAGE).join(TEAMS_ENTRY);
443        if candidate.is_file() {
444            return Ok(candidate);
445        }
446        searched.push(candidate.display().to_string());
447    }
448    Err(TeamsError::NoEntry {
449        searched: searched.join(", "),
450    })
451}
452
453/// Where npm installs global packages (`npm root -g`), when npm is present.
454fn global_npm_root() -> Option<PathBuf> {
455    let output = std::process::Command::new(resolve_program("npm"))
456        .args(["root", "-g"])
457        .stdin(std::process::Stdio::null())
458        .stderr(std::process::Stdio::null())
459        .output()
460        .ok()?;
461    if !output.status.success() {
462        return None;
463    }
464    let text = String::from_utf8_lossy(&output.stdout);
465    let root = text.trim();
466    if root.is_empty() {
467        return None;
468    }
469    Some(PathBuf::from(root))
470}
471
472/// Render the per-platform service unit for this machine's teams daemon.
473///
474/// The node takes no `--root`: it serves the home its own environment
475/// resolves (`SUPERCODE_TEAMS_HOME`, else `<SUPERCODE_HOME>/teams`), so the
476/// unit names the listen address and nothing else. A port of `0` means the
477/// node picks one and publishes it in `<home>/node.json`.
478///
479/// On Windows it is a per-user Scheduled Task, rendered exactly as a connector's is (see [`windows_task`]).
480pub fn service_unit(home: &Path, entry: &Path, node: &str) -> Result<ServiceUnit, TeamsError> {
481    let home_display = home.display().to_string();
482    let entry_display = entry.display().to_string();
483    // A control character would break out of any unit's syntax (a plist string, a systemd line, task XML).
484    for value in [home_display.as_str(), entry_display.as_str(), node] {
485        service_text(value)?;
486    }
487    if cfg!(windows) {
488        let log_path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.log"));
489        return windows_task(
490            &home.join(SERVICE_DIR).join(unit_file_name()),
491            SERVICE_NAME,
492            &format!("supercode teams machine daemon ({home_display})"),
493            &service_env_path(home, SERVICE_NAME),
494            &[
495                ("SUPERCODE_TEAMS_HOME", home_display.as_str()),
496                ("SUPERCODE_TEAMS_LOG", &log_path.display().to_string()),
497            ],
498            node,
499            &[entry_display.as_str(), "machine", "start"],
500            &home_display,
501        );
502    }
503    if cfg!(target_os = "macos") {
504        let path = home.join(SERVICE_DIR).join(format!("{SERVICE_NAME}.plist"));
505        let text = format!(
506            r#"<?xml version="1.0" encoding="UTF-8"?>
507<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
508<plist version="1.0">
509<dict>
510  <key>Label</key><string>{SERVICE_NAME}</string>
511  <key>ProgramArguments</key>
512  <array>
513    <string>{node}</string>
514    <string>{entry_display}</string>
515    <string>machine</string>
516    <string>start</string>
517  </array>
518  <key>EnvironmentVariables</key>
519  <dict>
520    <key>SUPERCODE_TEAMS_HOME</key><string>{home_display}</string>
521  </dict>
522  <key>RunAtLoad</key><true/>
523  <key>KeepAlive</key><true/>
524  <key>StandardOutPath</key><string>{home_display}/service/teams-machine.out.log</string>
525  <key>StandardErrorPath</key><string>{home_display}/service/teams-machine.err.log</string>
526</dict>
527</plist>
528"#
529        );
530        let install = format!("launchctl bootstrap gui/$(id -u) {}", path.display());
531        Ok(ServiceUnit {
532            kind: "launchd",
533            path,
534            text,
535            install_command: install,
536            files: Vec::new(),
537        })
538    } else {
539        let path = home
540            .join(SERVICE_DIR)
541            .join(format!("{SERVICE_NAME}.service"));
542        let text = format!(
543            "[Unit]\n\
544             Description=supercode teams machine daemon ({home_display})\n\
545             After=network.target\n\
546             \n\
547             [Service]\n\
548             Environment=SUPERCODE_TEAMS_HOME={home_display}\n\
549             ExecStart={node} {entry_display} machine start\n\
550             Restart=on-failure\n\
551             KillSignal=SIGTERM\n\
552             \n\
553             [Install]\n\
554             WantedBy=default.target\n"
555        );
556        let install = format!(
557            "systemctl --user link {} && systemctl --user enable --now {SERVICE_NAME}",
558            path.display()
559        );
560        Ok(ServiceUnit {
561            kind: "systemd",
562            path,
563            text,
564            install_command: install,
565            files: Vec::new(),
566        })
567    }
568}
569
570/// Write a rendered unit under `<home>/service/`.
571pub fn write_unit(unit: &ServiceUnit) -> Result<(), TeamsError> {
572    if let Some(parent) = unit.path.parent() {
573        std::fs::create_dir_all(parent).map_err(|source| TeamsError::File {
574            path: unit.path.clone(),
575            source,
576        })?;
577    }
578    std::fs::write(&unit.path, &unit.text).map_err(|source| TeamsError::File {
579        path: unit.path.clone(),
580        source,
581    })?;
582    for (path, text) in &unit.files {
583        std::fs::write(path, text).map_err(|source| TeamsError::File {
584            path: path.clone(),
585            source,
586        })?;
587    }
588    Ok(())
589}
590
591/// Run a service-manager command and return (success, stdout+stderr).
592fn run_tool(program: &str, args: &[&str]) -> Result<(bool, String), std::io::Error> {
593    let output = std::process::Command::new(program).args(args).output()?;
594    let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
595    text.push_str(&String::from_utf8_lossy(&output.stderr));
596    Ok((output.status.success(), text.trim().to_string()))
597}
598
599#[cfg(target_os = "macos")]
600fn gui_domain() -> String {
601    // SAFETY: `getuid` reads this process's own real user id and cannot fail.
602    format!("gui/{}", unsafe { libc::getuid() })
603}
604
605/// What the platform's service manager says about the teams daemon unit.
606///
607/// Never starts or installs anything.
608pub fn service_status() -> ServiceState {
609    platform_status()
610}
611
612#[cfg(target_os = "macos")]
613fn platform_status() -> ServiceState {
614    let label = SERVICE_NAME.to_string();
615    let target = format!("{}/{SERVICE_NAME}", gui_domain());
616    match run_tool("launchctl", &["print", &target]) {
617        Ok((true, text)) => ServiceState {
618            kind: "launchd",
619            label,
620            installed: true,
621            pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
622            detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
623        },
624        Ok((false, _)) => ServiceState {
625            kind: "launchd",
626            label,
627            installed: false,
628            pid: None,
629            detail: format!("not bootstrapped in {}", gui_domain()),
630        },
631        Err(error) => ServiceState {
632            kind: "launchd",
633            label,
634            installed: false,
635            pid: None,
636            detail: format!("launchctl unavailable: {error}"),
637        },
638    }
639}
640
641#[cfg(all(unix, not(target_os = "macos")))]
642fn platform_status() -> ServiceState {
643    let label = SERVICE_NAME.to_string();
644    match run_tool("systemctl", &["--user", "is-active", SERVICE_NAME]) {
645        Ok((active, text)) => {
646            let known = run_tool("systemctl", &["--user", "is-enabled", SERVICE_NAME])
647                .map(|(ok, _)| ok)
648                .unwrap_or(false);
649            ServiceState {
650                kind: "systemd",
651                label,
652                installed: active || known,
653                pid: None,
654                detail: if text.is_empty() {
655                    "unknown".into()
656                } else {
657                    text
658                },
659            }
660        }
661        Err(error) => ServiceState {
662            kind: "systemd",
663            label,
664            installed: false,
665            pid: None,
666            detail: format!("systemctl unavailable: {error}"),
667        },
668    }
669}
670
671#[cfg(not(unix))]
672fn platform_status() -> ServiceState {
673    named_service_status(SERVICE_NAME)
674}
675
676/// `key = value` out of a service manager's block output.
677#[cfg(target_os = "macos")]
678fn field_of(text: &str, key: &str) -> Option<String> {
679    text.lines()
680        .find_map(|line| line.trim().strip_prefix(key))
681        .map(|value| value.trim().to_string())
682}
683
684/// The file name the unit takes on this platform.
685fn unit_file_name() -> String {
686    if cfg!(windows) {
687        format!("{SERVICE_NAME}.xml")
688    } else if cfg!(target_os = "macos") {
689        format!("{SERVICE_NAME}.plist")
690    } else {
691        format!("{SERVICE_NAME}.service")
692    }
693}
694
695/// Render the unit, hand it to the platform's service manager, and start it.
696///
697/// Refuses a label the manager already holds rather than replacing it: two
698/// homes share one label, so an install that silently took it over would point
699/// a running node at a different folder.
700pub fn install_service(
701    home: &Path,
702    entry: &Path,
703    node: &str,
704) -> Result<(ServiceUnit, ServiceState), TeamsError> {
705    let existing = service_status();
706    if existing.installed {
707        return Err(TeamsError::Service {
708            action: "install",
709            detail: format!(
710                "`{}` is already installed ({}); `supercode teams machine uninstall` first",
711                existing.label, existing.detail
712            ),
713        });
714    }
715    let unit = service_unit(home, entry, &absolute_program(node))?;
716    write_unit(&unit)?;
717    platform_install(&unit)?;
718    Ok((unit, service_status()))
719}
720
721/// Install a rendered context connector. An identical installed unit is an
722/// idempotent success. A different unit in this home's own service folder is
723/// this home's connector with new parameters (a new build, PATH or folder), so
724/// it is replaced and restarted; a label the manager holds with no unit here
725/// belongs to another home and is refused.
726pub fn install_connector_service(
727    unit: &ServiceUnit,
728    label: &str,
729) -> Result<ServiceState, TeamsError> {
730    let existing = named_service_status(label);
731    if unit.path.exists() {
732        let old = std::fs::read_to_string(&unit.path).map_err(|source| TeamsError::File {
733            path: unit.path.clone(),
734            source,
735        })?;
736        // A Windows unit's environment lives in its companion file, so that is compared too.
737        let same = old == unit.text
738            && unit
739                .files
740                .iter()
741                .all(|(path, text)| std::fs::read_to_string(path).is_ok_and(|old| &old == text));
742        if same && existing.installed {
743            return Ok(existing);
744        }
745        if !same && existing.installed {
746            named_platform_uninstall(label)?;
747        }
748    } else if existing.installed {
749        return Err(TeamsError::Service {
750            action: "install",
751            detail: format!(
752                "service manager already owns `{label}` without its expected unit file"
753            ),
754        });
755    }
756    write_unit(unit)?;
757    named_platform_install(unit, label)?;
758    Ok(named_service_status(label))
759}
760
761/// Give every installed harness supercode's messaging tools: register
762/// `<supercode> message mcp` as a user-scope MCP server named `supercode`
763/// through each harness's own `mcp add`. A harness whose CLI is absent, or
764/// that already has the entry, is left as it is. One line per harness says
765/// what happened.
766pub fn register_message_tools(supercode: &Path) -> Vec<String> {
767    let program = supercode.display().to_string();
768    let mut report = Vec::new();
769    for (harness, get, add) in [
770        (
771            "claude",
772            vec!["mcp", "get", "supercode"],
773            vec![
774                "mcp",
775                "add",
776                "--scope",
777                "user",
778                "supercode",
779                "--",
780                &program,
781                "message",
782                "mcp",
783            ],
784        ),
785        (
786            "codex",
787            vec!["mcp", "get", "supercode"],
788            vec!["mcp", "add", "supercode", "--", &program, "message", "mcp"],
789        ),
790    ] {
791        let run = |args: &[&str]| {
792            std::process::Command::new(resolve_program(harness))
793                .args(args)
794                .stdin(std::process::Stdio::null())
795                .output()
796        };
797        match run(&get) {
798            Err(_) => report.push(format!("{harness}: not installed")),
799            Ok(found) if found.status.success() => {
800                report.push(format!("{harness}: already has supercode's tools"))
801            }
802            Ok(_) => match run(&add) {
803                Ok(added) if added.status.success() => report.push(format!(
804                    "{harness}: supercode's tools added (new sessions load them)"
805                )),
806                Ok(added) => report.push(format!(
807                    "{harness}: could not add supercode's tools: {}",
808                    String::from_utf8_lossy(&added.stderr).trim()
809                )),
810                Err(error) => report.push(format!(
811                    "{harness}: could not add supercode's tools: {error}"
812                )),
813            },
814        }
815    }
816    report
817}
818
819/// Stop and remove exactly one context connector service.
820pub fn uninstall_connector_service(
821    teams_home: &Path,
822    label: &str,
823) -> Result<ServiceState, TeamsError> {
824    named_platform_uninstall(label)?;
825    let unit = teams_home
826        .join(SERVICE_DIR)
827        .join(format!("{label}.{}", connector_unit_suffix()));
828    for path in [unit, service_env_path(teams_home, label)] {
829        match std::fs::remove_file(&path) {
830            Ok(()) => {}
831            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
832            Err(source) => return Err(TeamsError::File { path, source }),
833        }
834    }
835    Ok(named_service_status(label))
836}
837
838/// Read-only service-manager status for one context connector.
839pub fn connector_service_status(label: &str) -> ServiceState {
840    named_service_status(label)
841}
842
843#[cfg(target_os = "macos")]
844fn named_service_status(label: &str) -> ServiceState {
845    let target = format!("{}/{label}", gui_domain());
846    match run_tool("launchctl", &["print", &target]) {
847        Ok((true, text)) => ServiceState {
848            kind: "launchd",
849            label: label.into(),
850            installed: true,
851            pid: field_of(&text, "pid = ").and_then(|value| value.parse().ok()),
852            detail: field_of(&text, "state = ").unwrap_or_else(|| "loaded".into()),
853        },
854        Ok((false, _)) => ServiceState {
855            kind: "launchd",
856            label: label.into(),
857            installed: false,
858            pid: None,
859            detail: format!("not bootstrapped in {}", gui_domain()),
860        },
861        Err(error) => ServiceState {
862            kind: "launchd",
863            label: label.into(),
864            installed: false,
865            pid: None,
866            detail: format!("launchctl unavailable: {error}"),
867        },
868    }
869}
870
871#[cfg(all(unix, not(target_os = "macos")))]
872fn named_service_status(label: &str) -> ServiceState {
873    match run_tool("systemctl", &["--user", "is-active", label]) {
874        Ok((active, text)) => {
875            let known = run_tool("systemctl", &["--user", "is-enabled", label])
876                .map(|(ok, _)| ok)
877                .unwrap_or(false);
878            ServiceState {
879                kind: "systemd",
880                label: label.into(),
881                installed: active || known,
882                pid: None,
883                detail: if text.is_empty() {
884                    "unknown".into()
885                } else {
886                    text
887                },
888            }
889        }
890        Err(error) => ServiceState {
891            kind: "systemd",
892            label: label.into(),
893            installed: false,
894            pid: None,
895            detail: format!("systemctl unavailable: {error}"),
896        },
897    }
898}
899
900#[cfg(not(unix))]
901fn named_service_status(label: &str) -> ServiceState {
902    match run_tool("schtasks", &["/Query", "/TN", label, "/FO", "CSV", "/NH"]) {
903        // `"TaskName","Next Run Time","Status"`: the last field is the task's state, in the system's language.
904        Ok((true, text)) => ServiceState {
905            kind: "schtasks",
906            label: label.into(),
907            installed: true,
908            pid: None,
909            detail: text
910                .lines()
911                .next()
912                .and_then(|line| line.rsplit(',').next())
913                .map(|state| state.trim_matches('"').to_string())
914                .filter(|state| !state.is_empty())
915                .unwrap_or_else(|| "registered".into()),
916        },
917        Ok((false, _)) => ServiceState {
918            kind: "schtasks",
919            label: label.into(),
920            installed: false,
921            pid: None,
922            detail: "no scheduled task".into(),
923        },
924        Err(error) => ServiceState {
925            kind: "schtasks",
926            label: label.into(),
927            installed: false,
928            pid: None,
929            detail: format!("schtasks unavailable: {error}"),
930        },
931    }
932}
933
934#[cfg(target_os = "macos")]
935fn named_platform_install(unit: &ServiceUnit, _label: &str) -> Result<(), TeamsError> {
936    platform_install(unit)
937}
938#[cfg(all(unix, not(target_os = "macos")))]
939fn named_platform_install(unit: &ServiceUnit, label: &str) -> Result<(), TeamsError> {
940    let path = unit.path.display().to_string();
941    for args in [
942        vec!["--user", "link", path.as_str()],
943        vec!["--user", "enable", "--now", label],
944    ] {
945        let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
946            action: "install",
947            detail: format!("systemctl: {error}"),
948        })?;
949        if !ok {
950            return Err(TeamsError::Service {
951                action: "install",
952                detail: format!("systemctl {}: {text}", args.join(" ")),
953            });
954        }
955    }
956    Ok(())
957}
958#[cfg(not(unix))]
959fn named_platform_install(unit: &ServiceUnit, label: &str) -> Result<(), TeamsError> {
960    // Task Scheduler reads task XML only as UTF-16; the unit itself stays UTF-8 so an install can compare it.
961    let task = unit.path.with_extension("utf16.xml");
962    let bytes: Vec<u8> = [0xFF, 0xFE]
963        .into_iter()
964        .chain(unit.text.encode_utf16().flat_map(u16::to_le_bytes))
965        .collect();
966    std::fs::write(&task, bytes).map_err(|source| TeamsError::File {
967        path: task.clone(),
968        source,
969    })?;
970    let task_path = task.display().to_string();
971    let created = run_tool(
972        "schtasks",
973        &["/Create", "/TN", label, "/XML", task_path.as_str(), "/F"],
974    )
975    .map_err(|error| TeamsError::Service {
976        action: "install",
977        detail: format!("schtasks: {error}"),
978    })
979    .and_then(|(ok, text)| {
980        if ok {
981            Ok(())
982        } else {
983            Err(TeamsError::Service {
984                action: "install",
985                detail: format!("schtasks /Create: {text}"),
986            })
987        }
988    });
989    if let Err(error) = created {
990        let _ = std::fs::remove_file(&task);
991        return Err(error);
992    }
993    // A task already running keeps its old instance: Task Scheduler refuses a
994    // second one (0x800710E0) while `/Run` still reports success, so the
995    // replaced connector would go on running the old code. End it first; a
996    // task that is not running answers an error here, which is fine.
997    let _ = run_tool("schtasks", &["/End", "/TN", label]);
998    let result = [vec!["/Run", "/TN", label]].iter().try_for_each(|args| {
999        let (ok, text) = run_tool("schtasks", args).map_err(|error| TeamsError::Service {
1000            action: "install",
1001            detail: format!("schtasks: {error}"),
1002        })?;
1003        if ok {
1004            Ok(())
1005        } else {
1006            Err(TeamsError::Service {
1007                action: "install",
1008                detail: format!("schtasks {}: {text}", args[0]),
1009            })
1010        }
1011    });
1012    let _ = std::fs::remove_file(&task);
1013    result
1014}
1015
1016#[cfg(target_os = "macos")]
1017fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
1018    let target = format!("{}/{label}", gui_domain());
1019    let (ok, text) =
1020        run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
1021            action: "uninstall",
1022            detail: format!("launchctl: {error}"),
1023        })?;
1024    if !ok && !text.contains("No such process") && !text.contains("not find") {
1025        return Err(TeamsError::Service {
1026            action: "uninstall",
1027            detail: format!("launchctl bootout {target}: {text}"),
1028        });
1029    }
1030    // bootout returns before launchd has let the label go, and a bootstrap
1031    // in that window fails with an I/O error; wait for it to be released.
1032    for _ in 0..50 {
1033        if !named_service_status(label).installed {
1034            break;
1035        }
1036        std::thread::sleep(std::time::Duration::from_millis(100));
1037    }
1038    Ok(())
1039}
1040#[cfg(all(unix, not(target_os = "macos")))]
1041fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
1042    let _ = run_tool("systemctl", &["--user", "disable", "--now", label]);
1043    Ok(())
1044}
1045#[cfg(not(unix))]
1046fn named_platform_uninstall(label: &str) -> Result<(), TeamsError> {
1047    // The task goes first, so its minute trigger cannot start the service again while it is being stopped.
1048    let (ok, text) = run_tool("schtasks", &["/Delete", "/TN", label, "/F"]).map_err(|error| {
1049        TeamsError::Service {
1050            action: "uninstall",
1051            detail: format!("schtasks: {error}"),
1052        }
1053    })?;
1054    if !ok && named_service_status(label).installed {
1055        return Err(TeamsError::Service {
1056            action: "uninstall",
1057            detail: format!("schtasks /Delete: {text}"),
1058        });
1059    }
1060    // Deleting a task leaves what it started running, so the service is stopped by the one thing on its command
1061    // line that is its own: the environment file, on a node or its conhost. The path rides in the environment, not
1062    // the command line, so this query does not match itself.
1063    let env_path = service_env_path(&teams_home(), label);
1064    let stopped = std::process::Command::new("powershell.exe")
1065        .args([
1066            "-NoProfile",
1067            "-NonInteractive",
1068            "-Command",
1069            "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='conhost.exe'\" | Where-Object { $_.CommandLine -and $_.CommandLine.Contains($env:SUPERCODE_SERVICE_ENV_FILE) } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }",
1070        ])
1071        .env("SUPERCODE_SERVICE_ENV_FILE", env_path.display().to_string())
1072        .stdin(std::process::Stdio::null())
1073        .output();
1074    match stopped {
1075        Ok(output) if output.status.success() => Ok(()),
1076        Ok(output) => Err(TeamsError::Service {
1077            action: "uninstall",
1078            detail: format!(
1079                "the task is gone, but what it started may still run: {}",
1080                String::from_utf8_lossy(&output.stderr).trim()
1081            ),
1082        }),
1083        Err(error) => Err(TeamsError::Service {
1084            action: "uninstall",
1085            detail: format!(
1086                "the task is gone, but what it started may still run: powershell: {error}"
1087            ),
1088        }),
1089    }
1090}
1091
1092#[cfg(target_os = "macos")]
1093fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
1094    let path = unit.path.display().to_string();
1095    let (ok, text) =
1096        run_tool("launchctl", &["bootstrap", &gui_domain(), &path]).map_err(|error| {
1097            TeamsError::Service {
1098                action: "install",
1099                detail: format!("launchctl: {error}"),
1100            }
1101        })?;
1102    if !ok {
1103        return Err(TeamsError::Service {
1104            action: "install",
1105            detail: format!("launchctl bootstrap {}: {text}", gui_domain()),
1106        });
1107    }
1108    Ok(())
1109}
1110
1111/// Untested on this box (the receipt is macOS); these are the commands
1112/// `service_unit` prints as its `install_command`.
1113#[cfg(all(unix, not(target_os = "macos")))]
1114fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
1115    let path = unit.path.display().to_string();
1116    for args in [
1117        vec!["--user", "link", path.as_str()],
1118        vec!["--user", "enable", "--now", SERVICE_NAME],
1119    ] {
1120        let (ok, text) = run_tool("systemctl", &args).map_err(|error| TeamsError::Service {
1121            action: "install",
1122            detail: format!("systemctl: {error}"),
1123        })?;
1124        if !ok {
1125            return Err(TeamsError::Service {
1126                action: "install",
1127                detail: format!("systemctl {}: {text}", args.join(" ")),
1128            });
1129        }
1130    }
1131    Ok(())
1132}
1133
1134#[cfg(not(unix))]
1135fn platform_install(unit: &ServiceUnit) -> Result<(), TeamsError> {
1136    named_platform_install(unit, SERVICE_NAME)
1137}
1138
1139/// Stop and unregister the unit, and remove the rendered file.
1140///
1141/// Idempotent: a unit the manager does not hold is not an error, because the
1142/// state the operator asked for is the state they get.
1143pub fn uninstall_service(home: &Path) -> Result<ServiceState, TeamsError> {
1144    platform_uninstall()?;
1145    let unit_path = home.join(SERVICE_DIR).join(unit_file_name());
1146    // A Windows unit's environment file goes with it; elsewhere there is none and its absence is fine.
1147    for path in [unit_path, service_env_path(home, SERVICE_NAME)] {
1148        match std::fs::remove_file(&path) {
1149            Ok(()) => {}
1150            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1151            Err(source) => return Err(TeamsError::File { path, source }),
1152        }
1153    }
1154    // `launchctl bootout` returns before the job is torn down, so the state
1155    // this reports is the settled one, not the manager mid-teardown.
1156    let mut state = service_status();
1157    for _ in 0..40 {
1158        if !state.installed {
1159            break;
1160        }
1161        std::thread::sleep(std::time::Duration::from_millis(100));
1162        state = service_status();
1163    }
1164    Ok(state)
1165}
1166
1167#[cfg(target_os = "macos")]
1168fn platform_uninstall() -> Result<(), TeamsError> {
1169    let target = format!("{}/{SERVICE_NAME}", gui_domain());
1170    let (ok, text) =
1171        run_tool("launchctl", &["bootout", &target]).map_err(|error| TeamsError::Service {
1172            action: "uninstall",
1173            detail: format!("launchctl: {error}"),
1174        })?;
1175    // `bootout` on a label nobody holds says so and exits non-zero.
1176    if !ok && !text.contains("No such process") && !text.contains("not find") {
1177        return Err(TeamsError::Service {
1178            action: "uninstall",
1179            detail: format!("launchctl bootout {target}: {text}"),
1180        });
1181    }
1182    Ok(())
1183}
1184
1185#[cfg(all(unix, not(target_os = "macos")))]
1186fn platform_uninstall() -> Result<(), TeamsError> {
1187    let _ = run_tool("systemctl", &["--user", "disable", "--now", SERVICE_NAME]);
1188    Ok(())
1189}
1190
1191#[cfg(not(unix))]
1192fn platform_uninstall() -> Result<(), TeamsError> {
1193    named_platform_uninstall(SERVICE_NAME)
1194}
1195
1196#[cfg(test)]
1197mod connector_service_tests {
1198    use super::*;
1199
1200    #[test]
1201    fn connector_unit_is_context_scoped_and_contains_no_credential() {
1202        let unit = connector_service_unit(
1203            Path::new("/tmp/teams home"),
1204            Path::new("/tmp/supercode home"),
1205            Path::new("/tmp/sdk/teams/bin/teams.mjs"),
1206            "/usr/bin/node",
1207            Path::new("/tmp/bin/supercode"),
1208            "work",
1209            Path::new("/tmp/project with spaces"),
1210            "srv_1",
1211            "team_1",
1212        )
1213        .unwrap();
1214        assert!(unit.text.contains("teams"));
1215        assert!(unit.text.contains("connect"));
1216        assert!(unit.text.contains("work"));
1217        assert!(!unit.text.contains("credential"));
1218        assert!(unit
1219            .path
1220            .file_name()
1221            .unwrap()
1222            .to_string_lossy()
1223            .contains(&connector_service_name("srv_1", "team_1", "work")));
1224    }
1225
1226    #[test]
1227    fn connector_labels_separate_context_and_team() {
1228        assert_ne!(
1229            connector_service_name("srv", "team-a", "work"),
1230            connector_service_name("srv", "team-b", "work")
1231        );
1232        assert_ne!(
1233            connector_service_name("srv", "team-a", "work"),
1234            connector_service_name("srv", "team-a", "personal")
1235        );
1236    }
1237
1238    #[test]
1239    fn connector_unit_rejects_newlines_and_escapes_service_syntax() {
1240        let unsafe_unit = connector_service_unit(
1241            Path::new("/tmp/teams"),
1242            Path::new("/tmp/home"),
1243            Path::new("/tmp/entry"),
1244            "/usr/bin/node",
1245            Path::new("/tmp/supercode"),
1246            "bad\ncontext",
1247            Path::new("/tmp/work"),
1248            "srv",
1249            "team",
1250        );
1251        assert!(unsafe_unit.is_err());
1252        let render = |entry: &str, cwd: &str| {
1253            connector_service_unit(
1254                Path::new("/tmp/teams & logs"),
1255                Path::new("/tmp/home $x"),
1256                Path::new(entry),
1257                "/usr/bin/node",
1258                Path::new("/tmp/super\"code"),
1259                "work",
1260                Path::new(cwd),
1261                "srv",
1262                "team",
1263            )
1264        };
1265        if cfg!(windows) {
1266            // Task Scheduler would expand `%i`; such a path is refused rather than run as something else.
1267            assert!(render("/tmp/entry %i", "/tmp/a & b $").is_err());
1268            let unit = render("/tmp/entry", "/tmp/a & b $").unwrap();
1269            assert!(unit.text.contains("--cwd &quot;/tmp/a &amp; b $&quot;"));
1270            let (env_path, env_text) = &unit.files[0];
1271            assert!(env_path.ends_with(format!(
1272                "{}.env",
1273                connector_service_name("srv", "team", "work")
1274            )));
1275            assert!(env_text.contains("SUPERCODE_BIN='/tmp/super\"code'\n"));
1276            assert!(!unit.text.contains("super\"code"));
1277        } else if cfg!(target_os = "macos") {
1278            let unit = render("/tmp/entry %i", "/tmp/a & b % $").unwrap();
1279            assert!(unit.text.contains("&amp;"));
1280        } else {
1281            let unit = render("/tmp/entry %i", "/tmp/a & b % $").unwrap();
1282            assert!(unit.text.contains("%%"));
1283            assert!(unit.text.contains("$$"));
1284            assert!(unit.text.contains("\\\""));
1285        }
1286    }
1287
1288    #[test]
1289    fn machine_unit_runs_machine_start_against_its_home() {
1290        let unit = service_unit(
1291            Path::new("/tmp/teams & home"),
1292            Path::new("/tmp/sdk/teams/bin/teams.mjs"),
1293            "/usr/bin/node",
1294        )
1295        .unwrap();
1296        assert!(unit.text.contains("machine"));
1297        assert!(unit.text.contains("start"));
1298        assert!(unit.path.ends_with(format!("service/{}", unit_file_name())));
1299        assert!(service_unit(Path::new("/tmp/bad\nhome"), Path::new("/e"), "node").is_err());
1300        if cfg!(windows) {
1301            assert_eq!(unit.kind, "schtasks");
1302            // The home rides in the environment file; the task names only that file, node and the entry.
1303            let (env_path, env_text) = &unit.files[0];
1304            assert!(env_path.ends_with(format!("{SERVICE_NAME}.env")));
1305            assert!(env_text.contains("SUPERCODE_TEAMS_HOME='/tmp/teams & home'\n"));
1306            assert!(env_text.contains(&format!("{SERVICE_NAME}.log'\n")));
1307            assert!(unit.text.contains("--headless"));
1308            assert!(unit.text.contains("PT1M"));
1309            assert!(unit
1310                .text
1311                .contains("<WorkingDirectory>/tmp/teams &amp; home</WorkingDirectory>"));
1312            assert!(unit
1313                .text
1314                .contains(r"&quot;--env-file=/tmp/teams &amp; home\service\"));
1315            assert!(unit.text.contains("teams.mjs machine start</Arguments>"));
1316            assert!(service_unit(Path::new("/tmp/100%"), Path::new("/e"), "node").is_err());
1317        }
1318    }
1319
1320    #[test]
1321    fn windows_arguments_split_back_out_whole() {
1322        assert_eq!(windows_arg("plain"), "plain");
1323        assert_eq!(windows_arg(""), "\"\"");
1324        assert_eq!(
1325            windows_arg(r"C:\Program Files\nodejs\node.exe"),
1326            r#""C:\Program Files\nodejs\node.exe""#
1327        );
1328        assert_eq!(windows_arg(r#"say "hi""#), r#""say \"hi\"""#);
1329        // Backslashes before a quote, and before the closing quote, are doubled; elsewhere they are literal.
1330        assert_eq!(windows_arg(r#"a\"b"#), r#""a\\\"b""#);
1331        assert_eq!(windows_arg(r"C:\my dir\"), r#""C:\my dir\\""#);
1332    }
1333
1334    #[test]
1335    fn env_file_values_are_quoted_literally() {
1336        assert_eq!(env_file_value(r"C:\new\tab").unwrap(), r"'C:\new\tab'");
1337        assert_eq!(env_file_value("O'Brien").unwrap(), "`O'Brien`");
1338        assert_eq!(env_file_value("it's `x`").unwrap(), "\"it's `x`\"");
1339        assert!(env_file_value(r"it's `x` \n").is_err());
1340    }
1341}