Skip to content

Commit a5b042f

Browse files
committed
Render opt-in notes once on every path
Use fetched or persisted activation metadata after real downloads and one shared preview renderer for dry runs. Defer wet vendored output to the shared vendor step and cover agent, hosted, vendored, agent, apply, and list paths with a parameterized exact-once matrix. Keep unsupported save-only combinations as zero-request usage errors and preserve offline vendor activation metadata. Assisted-by: Claude Code:kimi-k3p1-pro
1 parent 54692fa commit a5b042f

8 files changed

Lines changed: 386 additions & 13 deletions

File tree

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,21 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
603603
}
604604
}
605605

606+
if !args.common.json && !args.common.silent {
607+
if let Some(notes) =
608+
crate::ui::opt_in_note_block(manifest.patches.iter().map(|(purl, record)| {
609+
crate::ui::OptInEntry {
610+
purl,
611+
uuid: &record.uuid,
612+
requires_opt_in: record.requires_opt_in,
613+
opt_in_note: record.opt_in_note.as_deref(),
614+
}
615+
}))
616+
{
617+
println!("\n{notes}");
618+
}
619+
}
620+
606621
if drifts.is_empty() {
607622
if args.common.json {
608623
let mut env = Envelope::new(Command::Apply);
@@ -1333,6 +1348,7 @@ pub async fn run(args: ApplyArgs) -> i32 {
13331348
_ => {}
13341349
}
13351350
}
1351+
print_vendor_opt_in_notes(&args.common).await;
13361352
return i32::from(matches!(vex_result, Some(ManifestlessVex::Failed(_))));
13371353
}
13381354

@@ -1502,6 +1518,27 @@ fn collect_apply_outcomes(
15021518
(applied, skipped)
15031519
}
15041520

1521+
async fn print_vendor_opt_in_notes(common: &GlobalArgs) {
1522+
if common.json || common.silent || common.is_global() {
1523+
return;
1524+
}
1525+
let Ok(state) = socket_patch_core::vendor::load_state(&common.cwd).await else {
1526+
return;
1527+
};
1528+
let entries = state.entries.values().filter_map(|entry| {
1529+
let record = entry.record.as_ref()?;
1530+
Some(crate::ui::OptInEntry {
1531+
purl: &entry.base_purl,
1532+
uuid: &record.uuid,
1533+
requires_opt_in: record.requires_opt_in,
1534+
opt_in_note: record.opt_in_note.as_deref(),
1535+
})
1536+
});
1537+
if let Some(notes) = crate::ui::opt_in_note_block(entries) {
1538+
println!("\n{notes}");
1539+
}
1540+
}
1541+
15051542
type OptInPatches = Vec<(String, String, Option<bool>, Option<String>)>;
15061543

15071544
fn opt_in_patches(manifest: &PatchManifest) -> OptInPatches {

‎crates/socket-patch-cli/src/commands/get.rs‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,7 +1356,7 @@ pub async fn run(args: GetArgs) -> i32 {
13561356
.await
13571357
}
13581358
};
1359-
if !quiet {
1359+
if !quiet && mode == super::scan::ScanMode::Agent {
13601360
if let Some(notes) =
13611361
crate::ui::opt_in_note_block([crate::ui::OptInEntry::from_response(&patch)])
13621362
{
@@ -1925,6 +1925,11 @@ async fn agent_dry_run(
19251925
for line in &lines {
19261926
println!("{line}");
19271927
}
1928+
if let Some(notes) =
1929+
crate::ui::opt_in_preview(selected.iter().map(crate::ui::OptInEntry::from_search))
1930+
{
1931+
println!("\n{notes}");
1932+
}
19281933
let action = if args.save_only {
19291934
"download and record"
19301935
} else {
@@ -2371,6 +2376,11 @@ async fn run_get_vendored(
23712376
emit(&env);
23722377
} else if !args.common.silent {
23732378
preview.print_human();
2379+
if let Some(notes) =
2380+
crate::ui::opt_in_preview(selected.iter().map(crate::ui::OptInEntry::from_search))
2381+
{
2382+
println!("\n{notes}");
2383+
}
23742384
}
23752385
return 0;
23762386
}
@@ -2501,11 +2511,6 @@ async fn run_get_vendored(
25012511
1
25022512
}
25032513
};
2504-
if !args.common.json && !args.common.silent {
2505-
if let Some(notes) = env.opt_in_note_block() {
2506-
println!("\n{notes}");
2507-
}
2508-
}
25092514
code
25102515
}
25112516

‎crates/socket-patch-cli/src/commands/list.rs‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,12 @@ fn field(indent: &str, label: &str, value: &str) -> Option<String> {
274274
}
275275

276276
/// One entry of the human listing (no trailing newline).
277+
#[cfg(test)]
277278
fn format_entry(entry: &ListEntry<'_>, color: bool) -> String {
279+
format_entry_inner(entry, color, true)
280+
}
281+
282+
fn format_entry_inner(entry: &ListEntry<'_>, color: bool, show_opt_in: bool) -> String {
278283
let patch = entry.record;
279284
let mut lines = vec![format!("Package: {}", sanitize(entry.purl))];
280285
lines.extend(field(" ", "UUID", &patch.uuid));
@@ -293,7 +298,7 @@ fn format_entry(entry: &ListEntry<'_>, color: bool) -> String {
293298
lines.extend(field(" ", "License", &patch.license));
294299
lines.extend(field(" ", "Exported", &patch.exported_at));
295300
lines.extend(field(" ", "Description", &patch.description));
296-
if patch.requires_opt_in == Some(true) {
301+
if show_opt_in && patch.requires_opt_in == Some(true) {
297302
if let Some(note) = &patch.opt_in_note {
298303
lines.extend(field(" ", "Opt-in required", note));
299304
}
@@ -348,7 +353,22 @@ fn format_listing(entries: &[ListEntry<'_>], color: bool) -> String {
348353
"Found {}:\n\n",
349354
crate::ui::plural(entries.len(), "patch", "patches")
350355
);
351-
let blocks: Vec<String> = entries.iter().map(|e| format_entry(e, color)).collect();
356+
let mut shown_opt_ins = std::collections::HashSet::new();
357+
let blocks: Vec<String> = entries
358+
.iter()
359+
.map(|entry| {
360+
let show_opt_in = match (
361+
entry.record.requires_opt_in,
362+
entry.record.opt_in_note.as_deref(),
363+
) {
364+
(Some(true), Some(note)) if !note.trim().is_empty() => {
365+
shown_opt_ins.insert((entry.record.uuid.as_str(), note))
366+
}
367+
_ => false,
368+
};
369+
format_entry_inner(entry, color, show_opt_in)
370+
})
371+
.collect();
352372
out.push_str(&blocks.join("\n\n"));
353373
out
354374
}
@@ -1231,6 +1251,21 @@ mod tests {
12311251
assert!(out.contains(" Summary: 漏洞\n"), "{out}");
12321252
}
12331253

1254+
#[test]
1255+
fn format_listing_deduplicates_identical_opt_in_notes_per_uuid() {
1256+
let mut manifest = sample_manifest();
1257+
let mut other = manifest.patches["pkg:npm/minimist@1.2.2"].clone();
1258+
other.requires_opt_in = Some(true);
1259+
other.opt_in_note = Some("Enable safe mode.".into());
1260+
manifest.patches.insert("pkg:npm/other@1.0.0".into(), other);
1261+
let first = manifest.patches.get_mut("pkg:npm/minimist@1.2.2").unwrap();
1262+
first.requires_opt_in = Some(true);
1263+
first.opt_in_note = Some("Enable safe mode.".into());
1264+
1265+
let out = format_listing(&combined_entries(Some(&manifest), &[], None), false);
1266+
assert_eq!(out.matches("Enable safe mode.").count(), 1, "{out}");
1267+
}
1268+
12341269
#[test]
12351270
fn format_listing_counts_and_separates_entries() {
12361271
assert_eq!(format_listing(&[], false), NO_PATCHES);

‎crates/socket-patch-cli/src/commands/scan/mod.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,7 +3615,7 @@ async fn scan_project(
36153615

36163616
if selected.is_empty() && (!reapply || already_recorded.is_empty()) {
36173617
if !silent {
3618-
if let Some(notes) = ui::opt_in_note_block(
3618+
if let Some(notes) = ui::opt_in_preview(
36193619
selected
36203620
.iter()
36213621
.chain(&already_recorded)
@@ -3689,8 +3689,8 @@ async fn scan_project(
36893689
}
36903690
}
36913691

3692-
if !silent && (vendor || report_only || args.common.dry_run) {
3693-
if let Some(notes) = ui::opt_in_note_block(
3692+
if !silent && (report_only || args.common.dry_run) {
3693+
if let Some(notes) = ui::opt_in_preview(
36943694
selected
36953695
.iter()
36963696
.chain(&already_recorded)

‎crates/socket-patch-cli/src/commands/scan/vendor_flow.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,20 @@ async fn vendor_under_lock(
588588
Some(Box::new(env)),
589589
));
590590
}
591+
if !common.json && !common.silent {
592+
if let Some(notes) =
593+
crate::ui::opt_in_note_block(manifest.patches.iter().map(|(purl, record)| {
594+
crate::ui::OptInEntry {
595+
purl,
596+
uuid: &record.uuid,
597+
requires_opt_in: record.requires_opt_in,
598+
opt_in_note: record.opt_in_note.as_deref(),
599+
}
600+
}))
601+
{
602+
println!("\n{notes}");
603+
}
604+
}
591605
migrate_legacy_manifest_records(common, &manifest_path, &manifest.patches, &mut env).await;
592606
if has_errors {
593607
env.mark_partial_failure();

‎crates/socket-patch-cli/src/ui/mod.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub(crate) use status::StatusLine;
2626
pub use text::ShownOptIn;
2727
pub(crate) use text::{
2828
agent_next_steps, manifest_error_message, next_steps, next_steps_installed, opt_in_note_block,
29-
plural, sentence_case, shell_word, short_uuid, sweep_failure, terminal_text, truncate,
30-
OptInEntry,
29+
opt_in_preview, plural, sentence_case, shell_word, short_uuid, sweep_failure, terminal_text,
30+
truncate, OptInEntry,
3131
};
3232

3333
/// [`text::no_patches_message`] for this run: the manifest path is named

‎crates/socket-patch-cli/src/ui/text.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ impl ShownOptIn {
330330
}
331331
}
332332

333+
pub(crate) fn opt_in_preview<'a>(
334+
entries: impl IntoIterator<Item = OptInEntry<'a>>,
335+
) -> Option<String> {
336+
opt_in_note_block(entries)
337+
}
338+
333339
pub(crate) fn opt_in_note_block<'a>(
334340
entries: impl IntoIterator<Item = OptInEntry<'a>>,
335341
) -> Option<String> {

0 commit comments

Comments
 (0)