1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Dependency resolution and installation for the addon `add`/`update` paths
//! (GH #727): a declared depth-1 dependency is part of the install consent
//! surface, so it must be previewed before the user is asked and installed
//! before anything is wired. Split out of `addon_cmd` to keep that file under
//! the LOC gate (`scripts/loc-gate.sh`, limit 1500 lines).
/// The addon's own **scoped** package reference (`@ns/slug`), derived from the
/// recorded install `source`. This is the self-dependency root handed to the
/// depth-1 resolver (GH #727, Finding A): a declared dependency whose name
/// equals it is the addon depending on its own pack and is refused.
///
/// Only a hosted `ctxpkg:@ns/slug@ver` source carries a namespace. A local
/// manifest (`"local"`) or a bundled-registry slug (`"registry"`) has none, so
/// a self-reference is unnameable and the guard is deliberately vacuous
/// (`None`) — never the bare `[addon] name` slug, which could never equal a
/// scoped `@ns/name` dependency and would only give the guard the false
/// appearance of being active.
pub(super) fn addon_self_ref(source: &str) -> Option<String> {
let spec = source.strip_prefix("ctxpkg:")?;
let remote_ref = crate::core::context_package::remote::parse_remote_ref(spec)?;
Some(format!("@{}/{}", remote_ref.namespace, remote_ref.name))
}
/// Re-resolve and install the declared dependencies of an addon (GH #727) —
/// used on `addon update`, where a dependency can move forward independently of
/// the addon binary.
pub(super) fn refresh_pack_dependencies(
deps: &[crate::core::context_package::manifest::PackageDependency],
root_name: Option<&str>,
args: &[String],
) {
if deps.iter().all(|d| d.optional) {
return;
}
let base = crate::core::context_package::remote::registry_base(
super::addon_cmd::flag_value(args, "--registry").as_deref(),
);
let token = crate::core::context_package::remote::publish_token(None);
let project_root = super::common::detect_project_root(args);
println!("Refreshing declared dependencies (depth-1) …");
if let Err(e) = super::pack_remote::install_declared_dependencies(
deps,
root_name,
&base,
token.as_deref(),
&project_root,
true,
) {
eprintln!("Warning: dependency refresh failed: {e}\n The addon update itself succeeded.");
}
}
/// Resolve the declared depth-1 dependencies of an addon so the caller can
/// show them in the consent preview (GH #727). Exits on a resolution failure —
/// nothing has been touched at this point.
///
/// This is a **preview only**: it picks the highest in-range version and does
/// not consult `ctxpkg.lock`. The authoritative versions that get wired into
/// `[mcp.env]` come from [`install_declared_deps`] (which honours the lockfile),
/// so in the rare case where the lockfile pins an older in-range version this
/// preview may list a newer version than the install actually lands. The wiring
/// is always correct; only the pre-consent print can differ.
pub(super) fn resolve_declared_deps(
deps: &[crate::core::context_package::manifest::PackageDependency],
root_name: Option<&str>,
args: &[String],
) -> Vec<crate::core::context_package::deps::ResolvedDep> {
if deps.iter().all(|d| d.optional) {
return Vec::new();
}
let base = crate::core::context_package::remote::registry_base(
super::addon_cmd::flag_value(args, "--registry").as_deref(),
);
let token = crate::core::context_package::remote::publish_token(None);
match crate::core::context_package::deps::resolve_dependencies(
deps,
root_name,
&base,
token.as_deref(),
) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
/// Install the declared dependencies **before** anything is wired and return
/// the [`ResolvedDep`]s that actually landed (locked versions honoured). A path
/// burned into `[mcp.env]` must exist before it is burned in, so a failed
/// dependency install wires nothing at all — and the wiring expands
/// `{pack_dir:}` against exactly this returned slice (GH #727, Finding B).
pub(super) fn install_declared_deps(
deps: &[crate::core::context_package::manifest::PackageDependency],
root_name: Option<&str>,
args: &[String],
) -> Vec<crate::core::context_package::deps::ResolvedDep> {
let base = crate::core::context_package::remote::registry_base(
super::addon_cmd::flag_value(args, "--registry").as_deref(),
);
let token = crate::core::context_package::remote::publish_token(None);
let project_root = super::common::detect_project_root(args);
match super::pack_remote::install_declared_dependencies(
deps,
root_name,
&base,
token.as_deref(),
&project_root,
false,
) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: dependency install failed: {e}\n Nothing was wired.");
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The pure part of the addon install path that GH #727 Finding A got
/// wrong: deriving the self-dependency root from the recorded install
/// source. A hosted `ctxpkg:@ns/slug@ver` source yields the scoped
/// `@ns/slug` the resolver's guard compares against; a `local` manifest or
/// a bundled `registry` slug has no namespace, so the guard is deliberately
/// vacuous (`None`) — the pre-fix code instead passed the bare `addon.name`
/// slug, which could never match a scoped dependency and silently disabled
/// the guard on this path.
#[test]
fn addon_self_ref_is_scoped_for_hosted_sources_and_none_otherwise() {
// Hosted pack: scoped `@ns/slug`, version pin stripped.
assert_eq!(
addon_self_ref("ctxpkg:@dasTholo/lean-md@0.2.0").as_deref(),
Some("@dasTholo/lean-md")
);
assert_eq!(
addon_self_ref("ctxpkg:@dasTholo/lean-md").as_deref(),
Some("@dasTholo/lean-md")
);
// No namespace ⇒ self-reference unnameable ⇒ guard vacuous.
assert_eq!(addon_self_ref("local"), None);
assert_eq!(addon_self_ref("registry"), None);
// A bare slug is never returned, so the bug (bare root) is unreachable.
assert_eq!(addon_self_ref("ctxpkg:demo"), None);
}
}