1use serde::Serialize;
13
14use crate::cli::depend::{AddArgs, AssessArgs, DependAction, DependArgs};
15use crate::depend::fragments::Fragment;
16use crate::depend::matrix::{self, Mode, Recommendation};
17use crate::depend::source::{ChannelEvidence, Source, TagStyle};
18use crate::depend::target::{Already, Target};
19use crate::depend::version::{self, Resolved};
20use crate::depend::{self, Channel, Kind, Manager, source, target};
21use crate::devshell::Presence;
22use crate::diagnostic::{Diagnostic, Reason};
23use crate::error::RkError;
24use crate::output::Output;
25
26#[derive(Debug, Serialize)]
28struct AssessReport<'a> {
29 schema: &'static str,
31 verdict: &'static str,
33 source: SourceView<'a>,
35 target: TargetView<'a>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 resolved: Option<&'a Resolved>,
40 dev: &'a [Recommendation],
42 #[serde(skip_serializing_if = "Option::is_none")]
44 prod: Option<&'a Recommendation>,
45 next: &'a [String],
47}
48
49#[derive(Debug, Serialize)]
51struct SourceView<'a> {
52 path: &'a str,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 tech: Option<&'static str>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 name: Option<&'a str>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 version: Option<&'a str>,
63 bins: &'a [String],
65 #[serde(skip_serializing_if = "Option::is_none")]
67 owner_repo: Option<&'a str>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 host: Option<&'a str>,
71 tag_style: TagStyle,
73 channels: &'a [ChannelEvidence],
75}
76
77#[derive(Debug, Serialize)]
79struct TargetView<'a> {
80 path: &'a str,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 tech: Option<&'static str>,
85 managers: Vec<ManagerRow<'a>>,
87 envrc_use_flake: bool,
89 already: &'a [Already],
91}
92
93#[derive(Debug, Serialize)]
95struct ManagerRow<'a> {
96 manager: Manager,
98 file: &'a str,
100}
101
102#[derive(Debug, Serialize)]
104struct AddReport<'a> {
105 schema: &'static str,
107 mode: &'static str,
109 kind: Kind,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 manager: Option<Manager>,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 manager_origin: Option<&'static str>,
117 channel: Channel,
119 landing: Mode,
121 target: &'a str,
123 source: &'a str,
125 name: &'a str,
127 version: &'a str,
129 tag: &'a str,
131 version_origin: &'static str,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 file: Option<&'a str>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 file_present: Option<Presence>,
139 written: &'a [String],
142 #[serde(skip_serializing_if = "Option::is_none")]
144 refusal: Option<&'a str>,
145 fragments: &'a [Fragment],
147 #[serde(skip_serializing_if = "Option::is_none")]
149 command: Option<&'a str>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 reason: Option<&'static str>,
153 freshness: &'a str,
155 next: &'a [String],
157}
158
159pub fn run(args: &DependArgs) -> Result<(), RkError> {
165 match &args.action {
166 DependAction::Assess(args) => assess(args),
167 DependAction::Add(args) => add(args),
168 }
169}
170
171fn assess(args: &AssessArgs) -> Result<(), RkError> {
173 let out = Output::new(args.json);
174 depend::reject_url(&args.source)?;
175 let source = source::observe(&args.source)?;
176 let target = target::observe(&args.target, source.name.as_deref())?;
177 let resolved = version::resolve(&source, None).ok();
178 let (dev, prod) = resolved.as_ref().map_or_else(
179 || (Vec::new(), None),
180 |resolved| {
181 (
182 matrix::recommend(&source, &target, Kind::Dev, resolved),
183 matrix::recommend(&source, &target, Kind::Prod, resolved)
184 .into_iter()
185 .next(),
186 )
187 },
188 );
189 let verdict = verdict(&source, resolved.as_ref(), &dev, prod.as_ref());
190 out.result_line(format!(
191 "source {}: {} {} {}",
192 source.path,
193 source.tech.unwrap_or("unknown technology"),
194 source.name.as_deref().unwrap_or("(unnamed)"),
195 source.version.as_deref().unwrap_or("(no version)")
196 ));
197 out.result_line(format!(
198 "channels: {}",
199 list(source.channels.iter().map(|c| c.channel.as_str()))
200 ));
201 out.result_line(format!(
202 "target {}: {}; managers {}",
203 target.path,
204 target.tech.unwrap_or("unknown technology"),
205 list(target.managers.iter().map(|m| m.file.as_str()))
206 ));
207 for already in &target.already {
208 out.result_line(format!(
209 "already named in {}:{}",
210 already.file, already.line
211 ));
212 }
213 for option in &dev {
214 out.result_line(option_line(option));
215 }
216 if let Some(option) = &prod {
217 out.result_line(option_line(option));
218 }
219 out.result_line(format!("verdict {verdict}"));
220 let next = assess_next(verdict, &target, &dev);
221 out.next(&next);
222 out.emit(&AssessReport {
223 schema: "rk.depend-assess/1",
224 verdict,
225 source: source_view(&source),
226 target: target_view(&target),
227 resolved: resolved.as_ref(),
228 dev: &dev,
229 prod: prod.as_ref(),
230 next: &next,
231 })
232}
233
234fn add(args: &AddArgs) -> Result<(), RkError> {
236 let out = Output::new(args.json);
237 depend::reject_url(&args.source)?;
238 let source = source::observe(&args.source)?;
239 let target = target::observe(&args.target, source.name.as_deref())?;
240 let resolved = version::resolve(&source, args.pin.as_deref())?;
241 let options = matrix::recommend(&source, &target, args.kind, &resolved);
242 let option = matrix::choose(&options, args.manager, args.channel)?;
243 let manager_origin = option.manager.map(|_| {
244 if args.manager.is_some() {
245 "argument"
246 } else {
247 "detected"
248 }
249 });
250 let file_present = option
251 .file
252 .as_deref()
253 .map(|file| Presence::of(&target.path.join(file)));
254 let mode = if args.apply { "apply" } else { "preview" };
255 let (written, refusal) = if args.apply {
256 seed_or_refuse(option, &target, file_present)?
257 } else {
258 (Vec::new(), None)
259 };
260 let name = source.name.as_deref().unwrap_or_default();
261 if args.apply {
262 for file in &written {
263 out.result_line(format!("wrote {file}"));
264 }
265 } else {
266 out.result_line("DRY RUN: rk depend add prints the fragment or the command; --apply seeds only a manager file the target lacks");
267 }
268 out.result_line(format!(
269 "{name} {} (tag {}, from the {})",
270 resolved.version, resolved.tag, resolved.origin
271 ));
272 render_option(out, option, file_present);
273 let next = add_next(option, &target, args.apply, &written);
274 out.next(&next);
275 out.emit(&AddReport {
276 schema: "rk.depend-add/1",
277 mode,
278 kind: option.kind,
279 manager: option.manager,
280 manager_origin,
281 channel: option.channel,
282 landing: option.mode,
283 target: target.path.as_str(),
284 source: source.path.as_str(),
285 name,
286 version: &resolved.version,
287 tag: &resolved.tag,
288 version_origin: resolved.origin,
289 file: option.file.as_deref(),
290 file_present,
291 written: &written,
292 refusal: refusal.as_deref(),
293 fragments: &option.fragments,
294 command: option.command.as_deref(),
295 reason: option.reason,
296 freshness: &option.freshness,
297 next: &next,
298 })?;
299 if args.apply && option.kind == Kind::Prod {
300 return Err(RkError::Usage(
301 "rk never edits Cargo.toml, pyproject.toml, or package.json; run the printed command instead of --apply".into(),
302 ));
303 }
304 let Some(message) = refusal else {
305 return Ok(());
306 };
307 Err(RkError::refusal(
308 Diagnostic::new(Reason::DestructiveRefusal, message)
309 .expected("a target with no file for the manager, or the fragments applied by hand")
310 .target_state("nothing was written; the owned file is byte-identical"),
311 ))
312}
313
314fn seed_or_refuse(
317 option: &Recommendation,
318 target: &Target,
319 file_present: Option<Presence>,
320) -> Result<(Vec<String>, Option<String>), RkError> {
321 match (option.mode, &option.seed, file_present) {
322 (Mode::Fragment, Some(seed), Some(Presence::Absent)) => {
323 crate::atomic::write(
324 target.path.join(&seed.file).as_std_path(),
325 seed.text.as_bytes(),
326 )?;
327 Ok((vec![seed.file.clone()], None))
328 }
329 (Mode::Fragment, _, _) => Ok((
330 Vec::new(),
331 Some(format!(
332 "the target already carries {}; rk depend add never edits a file the target owns",
333 option.file.as_deref().unwrap_or("its manager file")
334 )),
335 )),
336 (Mode::Manual, _, _) => Err(RkError::Usage(format!(
337 "the pair is manual ({}); apply the printed text by hand",
338 option.reason.unwrap_or("no reason")
339 ))),
340 (Mode::Native, _, _) => Ok((Vec::new(), None)),
341 }
342}
343
344fn render_option(out: Output, option: &Recommendation, file_present: Option<Presence>) {
347 out.result_line(option_line(option));
348 if let (Some(file), Some(present)) = (option.file.as_deref(), file_present) {
349 out.result_line(match present {
350 Presence::Present => {
351 format!("{file} present: the target owns it, so the fragments are applied by hand")
352 }
353 Presence::Absent => format!("{file} absent: --apply seeds it"),
354 });
355 }
356 for fragment in &option.fragments {
357 out.result_line(format!(
358 "--- {} into {} ({} at {}){}",
359 fragment.id,
360 fragment.file,
361 fragment.placement,
362 fragment.anchor.path,
363 match fragment.present {
364 Some(true) => ": already present",
365 Some(false) => ": missing",
366 None => ": not judged",
367 }
368 ));
369 out.result_line(&fragment.text);
370 }
371 if let Some(command) = &option.command {
372 out.result_line(format!("run: {command}"));
373 }
374}
375
376fn verdict(
381 source: &Source,
382 resolved: Option<&Resolved>,
383 dev: &[Recommendation],
384 prod: Option<&Recommendation>,
385) -> &'static str {
386 if source.channels.is_empty() {
387 return "source-unknown";
388 }
389 if resolved.is_none() {
390 return "version-unknown";
391 }
392 let lands = |option: &Recommendation| option.mode != Mode::Manual;
393 if dev.iter().any(lands) || prod.is_some_and(lands) {
394 "ready"
395 } else {
396 "manual-only"
397 }
398}
399
400fn option_line(option: &Recommendation) -> String {
401 use std::fmt::Write as _;
402 let kind = option
403 .manager
404 .map_or_else(|| "prod".to_owned(), |m| format!("dev {}", m.as_str()));
405 let mut line = format!(
406 "{kind} via {}: {}",
407 option.channel.as_str(),
408 mode_word(option.mode)
409 );
410 if let Some(reason) = option.reason {
411 let _ = write!(line, " ({reason})");
412 }
413 if let Some(command) = &option.command {
414 let _ = write!(line, " {command}");
415 }
416 if option.manager.is_some() && !option.manager_present {
417 line.push_str(" (seeds the file)");
418 }
419 line
420}
421
422const fn mode_word(mode: Mode) -> &'static str {
423 match mode {
424 Mode::Fragment => "fragment",
425 Mode::Native => "native",
426 Mode::Manual => "manual",
427 }
428}
429
430fn list<'a>(items: impl Iterator<Item = &'a str>) -> String {
431 let joined: Vec<&str> = items.collect();
432 if joined.is_empty() {
433 "none".to_owned()
434 } else {
435 joined.join(", ")
436 }
437}
438
439fn source_view(source: &Source) -> SourceView<'_> {
440 SourceView {
441 path: source.path.as_str(),
442 tech: source.tech,
443 name: source.name.as_deref(),
444 version: source.version.as_deref(),
445 bins: &source.bins,
446 owner_repo: source.owner_repo.as_deref(),
447 host: source.host.as_deref(),
448 tag_style: source.tag_style,
449 channels: &source.channels,
450 }
451}
452
453fn target_view(target: &Target) -> TargetView<'_> {
454 TargetView {
455 path: target.path.as_str(),
456 tech: target.tech,
457 managers: target
458 .managers
459 .iter()
460 .map(|m| ManagerRow {
461 manager: m.manager,
462 file: &m.file,
463 })
464 .collect(),
465 envrc_use_flake: target.envrc_use_flake,
466 already: &target.already,
467 }
468}
469
470fn assess_next(verdict: &str, target: &Target, dev: &[Recommendation]) -> Vec<String> {
471 let mut next = Vec::new();
472 match verdict {
473 "source-unknown" => {
474 next.push("the source declares no channel this binary reads: a Cargo.toml package, a flake with packages, a pyproject project, a package.json, or dist-workspace.toml".to_owned());
475 }
476 "version-unknown" => {
477 next.push(
478 "the source declares no version; rk depend add --pin <version> names the release to pin"
479 .to_owned(),
480 );
481 }
482 "manual-only" => {
483 next.push(
484 "every pair is a hand edit; apply the printed text with the reason in view"
485 .to_owned(),
486 );
487 }
488 _ => {
489 if target.managers.len() > 1 {
490 next.push("rk depend add --kind dev --manager <manager> (the target carries more than one)".to_owned());
491 } else if target.managers.is_empty() && !dev.is_empty() {
492 next.push(
493 "rk depend add --kind dev --manager <manager> seeds the file the target lacks"
494 .to_owned(),
495 );
496 } else {
497 next.push("rk depend add --kind dev|prod previews the landing".to_owned());
498 }
499 }
500 }
501 next
502}
503
504fn add_next(
505 option: &Recommendation,
506 target: &Target,
507 applied: bool,
508 written: &[String],
509) -> Vec<String> {
510 let mut next = Vec::new();
511 match option.mode {
512 Mode::Native => {
513 if let Some(command) = &option.command {
514 next.push(format!(
515 "run {command} in the target, then commit the manifest and its lock"
516 ));
517 }
518 }
519 Mode::Manual => {
520 next.push(format!(
521 "apply the printed text by hand: {}",
522 option.reason.unwrap_or("manual")
523 ));
524 }
525 Mode::Fragment => {
526 if !applied && option.manager_present {
527 next.push("apply each fragment at its anchor in the order printed".to_owned());
528 } else if !applied {
529 next.push("rk depend add --apply seeds the manager file".to_owned());
530 }
531 if written.iter().any(|f| f == "flake.nix") && !target.envrc_use_flake {
532 next.push(
533 "let direnv load the flake from .envrc, or enter the shell with nix develop"
534 .to_owned(),
535 );
536 }
537 if let Some(manager) = option.manager {
538 next.push(match manager {
539 Manager::Flake => {
540 "nix flake lock, then commit flake.nix and flake.lock".to_owned()
541 }
542 Manager::Mise => "mise install, then commit the mise configuration".to_owned(),
543 Manager::Asdf => "asdf install, then commit .tool-versions".to_owned(),
544 Manager::Devbox => {
545 "devbox install, then commit devbox.json and devbox.lock".to_owned()
546 }
547 });
548 }
549 }
550 }
551 if !option.freshness.is_empty() {
552 next.push(format!(
553 "freshness is the manager's own verb: {}",
554 option.freshness
555 ));
556 }
557 next
558}
559
560#[cfg(test)]
561mod tests {
562 #![allow(clippy::expect_used)]
563
564 use camino::Utf8PathBuf;
565
566 use super::{AddReport, AssessReport, ManagerRow, SourceView, TargetView};
567 use crate::depend::fragments::{Anchor, Fragment};
568 use crate::depend::matrix::{Mode, Recommendation, Seed};
569 use crate::depend::source::{ChannelEvidence, TagStyle};
570 use crate::depend::target::Already;
571 use crate::depend::version::Resolved;
572 use crate::depend::{Channel, Kind, Manager};
573 use crate::devshell::Presence;
574
575 fn fragment() -> Fragment {
576 Fragment {
577 id: "mise-tool",
578 file: "mise.toml".to_owned(),
579 role: "the pinned tool entry",
580 placement: "insert-into-table",
581 anchor: Anchor {
582 kind: "table",
583 path: "tools".to_owned(),
584 needle: Some("[tools]"),
585 },
586 text: "\"cargo:sample-tool\" = \"1.4.0\"".to_owned(),
587 present: Some(false),
588 }
589 }
590
591 #[test]
593 #[allow(clippy::too_many_lines)]
594 fn the_depend_assess_schema_snapshot_holds() {
595 let channels = vec![ChannelEvidence {
596 channel: Channel::Crates,
597 evidence: vec!["Cargo.toml names a package".to_owned()],
598 }];
599 let bins = vec!["sam".to_owned()];
600 let already = vec![Already {
601 manager: Manager::Mise,
602 file: "mise.toml".to_owned(),
603 line: 3,
604 }];
605 let resolved = Resolved {
606 version: "1.4.0".to_owned(),
607 tag: "v1.4.0".to_owned(),
608 origin: "source-tree",
609 };
610 let dev = vec![Recommendation {
611 kind: Kind::Dev,
612 manager: Some(Manager::Mise),
613 manager_present: true,
614 channel: Channel::Crates,
615 mode: Mode::Fragment,
616 file: Some("mise.toml".to_owned()),
617 fragments: vec![fragment()],
618 seed: None,
619 command: None,
620 reason: None,
621 freshness: "mise upgrade --bump cargo:sample-tool".to_owned(),
622 }];
623 let prod = Recommendation {
624 kind: Kind::Prod,
625 manager: None,
626 manager_present: false,
627 channel: Channel::Crates,
628 mode: Mode::Native,
629 file: None,
630 fragments: Vec::new(),
631 seed: None,
632 command: Some("cargo add sample-tool@1.4.0".to_owned()),
633 reason: None,
634 freshness: "cargo update -p sample-tool".to_owned(),
635 };
636 let next = vec!["rk depend add --kind dev|prod previews the landing".to_owned()];
637 let report = AssessReport {
638 schema: "rk.depend-assess/1",
639 verdict: "ready",
640 source: SourceView {
641 path: "/srv/sample",
642 tech: Some("rust"),
643 name: Some("sample-tool"),
644 version: Some("1.4.0"),
645 bins: &bins,
646 owner_repo: Some("acme/sample-tool"),
647 host: Some("github.com"),
648 tag_style: TagStyle::Prefixed,
649 channels: &channels,
650 },
651 target: TargetView {
652 path: "/srv/widget",
653 tech: Some("rust"),
654 managers: vec![ManagerRow {
655 manager: Manager::Mise,
656 file: "mise.toml",
657 }],
658 envrc_use_flake: false,
659 already: &already,
660 },
661 resolved: Some(&resolved),
662 dev: &dev,
663 prod: Some(&prod),
664 next: &next,
665 };
666 assert_eq!(
667 serde_json::to_string(&report).expect("a report serializes"),
668 r#"{"schema":"rk.depend-assess/1","verdict":"ready","source":{"path":"/srv/sample","tech":"rust","name":"sample-tool","version":"1.4.0","bins":["sam"],"owner_repo":"acme/sample-tool","host":"github.com","tag_style":"prefixed","channels":[{"channel":"crates","evidence":["Cargo.toml names a package"]}]},"target":{"path":"/srv/widget","tech":"rust","managers":[{"manager":"mise","file":"mise.toml"}],"envrc_use_flake":false,"already":[{"manager":"mise","file":"mise.toml","line":3}]},"resolved":{"version":"1.4.0","tag":"v1.4.0","origin":"source-tree"},"dev":[{"kind":"dev","manager":"mise","manager_present":true,"channel":"crates","mode":"fragment","file":"mise.toml","fragments":[{"id":"mise-tool","file":"mise.toml","role":"the pinned tool entry","placement":"insert-into-table","anchor":{"kind":"table","path":"tools","needle":"[tools]"},"text":"\"cargo:sample-tool\" = \"1.4.0\"","present":false}],"freshness":"mise upgrade --bump cargo:sample-tool"}],"prod":{"kind":"prod","manager_present":false,"channel":"crates","mode":"native","fragments":[],"command":"cargo add sample-tool@1.4.0","freshness":"cargo update -p sample-tool"},"next":["rk depend add --kind dev|prod previews the landing"]}"#
669 );
670 let bare = AssessReport {
671 schema: "rk.depend-assess/1",
672 verdict: "source-unknown",
673 source: SourceView {
674 path: "/srv/sample",
675 tech: None,
676 name: None,
677 version: None,
678 bins: &[],
679 owner_repo: None,
680 host: None,
681 tag_style: TagStyle::Unknown,
682 channels: &[],
683 },
684 target: TargetView {
685 path: "/srv/widget",
686 tech: None,
687 managers: Vec::new(),
688 envrc_use_flake: false,
689 already: &[],
690 },
691 resolved: None,
692 dev: &[],
693 prod: None,
694 next: &[],
695 };
696 assert_eq!(
697 serde_json::to_string(&bare).expect("a report serializes"),
698 r#"{"schema":"rk.depend-assess/1","verdict":"source-unknown","source":{"path":"/srv/sample","bins":[],"tag_style":"unknown","channels":[]},"target":{"path":"/srv/widget","managers":[],"envrc_use_flake":false,"already":[]},"dev":[],"next":[]}"#,
699 "an unknown value is omitted, never null"
700 );
701 }
702
703 #[test]
705 fn the_depend_add_schema_snapshot_holds() {
706 let fragments = vec![fragment()];
707 let written = vec!["mise.toml".to_owned()];
708 let next = vec!["mise install, then commit the mise configuration".to_owned()];
709 let report = AddReport {
710 schema: "rk.depend-add/1",
711 mode: "apply",
712 kind: Kind::Dev,
713 manager: Some(Manager::Mise),
714 manager_origin: Some("detected"),
715 channel: Channel::Crates,
716 landing: Mode::Fragment,
717 target: "/srv/widget",
718 source: "/srv/sample",
719 name: "sample-tool",
720 version: "1.4.0",
721 tag: "v1.4.0",
722 version_origin: "source-tree",
723 file: Some("mise.toml"),
724 file_present: Some(Presence::Absent),
725 written: &written,
726 refusal: None,
727 fragments: &fragments,
728 command: None,
729 reason: None,
730 freshness: "mise upgrade --bump cargo:sample-tool",
731 next: &next,
732 };
733 assert_eq!(
734 serde_json::to_string(&report).expect("a report serializes"),
735 r#"{"schema":"rk.depend-add/1","mode":"apply","kind":"dev","manager":"mise","manager_origin":"detected","channel":"crates","landing":"fragment","target":"/srv/widget","source":"/srv/sample","name":"sample-tool","version":"1.4.0","tag":"v1.4.0","version_origin":"source-tree","file":"mise.toml","file_present":"absent","written":["mise.toml"],"fragments":[{"id":"mise-tool","file":"mise.toml","role":"the pinned tool entry","placement":"insert-into-table","anchor":{"kind":"table","path":"tools","needle":"[tools]"},"text":"\"cargo:sample-tool\" = \"1.4.0\"","present":false}],"freshness":"mise upgrade --bump cargo:sample-tool","next":["mise install, then commit the mise configuration"]}"#
736 );
737 let bare = AddReport {
738 schema: "rk.depend-add/1",
739 mode: "preview",
740 kind: Kind::Prod,
741 manager: None,
742 manager_origin: None,
743 channel: Channel::Crates,
744 landing: Mode::Native,
745 target: "/srv/widget",
746 source: "/srv/sample",
747 name: "sample-tool",
748 version: "1.4.0",
749 tag: "v1.4.0",
750 version_origin: "argument",
751 file: None,
752 file_present: None,
753 written: &[],
754 refusal: None,
755 fragments: &[],
756 command: Some("cargo add sample-tool@1.4.0"),
757 reason: None,
758 freshness: "cargo update -p sample-tool",
759 next: &[],
760 };
761 assert_eq!(
762 serde_json::to_string(&bare).expect("a report serializes"),
763 r#"{"schema":"rk.depend-add/1","mode":"preview","kind":"prod","channel":"crates","landing":"native","target":"/srv/widget","source":"/srv/sample","name":"sample-tool","version":"1.4.0","tag":"v1.4.0","version_origin":"argument","written":[],"fragments":[],"command":"cargo add sample-tool@1.4.0","freshness":"cargo update -p sample-tool","next":[]}"#,
764 "an unknown value is omitted, never null"
765 );
766 let seed = Seed {
767 file: "mise.toml".to_owned(),
768 text: "[tools]\n".to_owned(),
769 };
770 assert_eq!(
771 serde_json::to_string(&seed).expect("a seed serializes"),
772 r#"{"file":"mise.toml","text":"[tools]\n"}"#
773 );
774 let _ = Utf8PathBuf::from("/srv/widget");
775 }
776}