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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! Execution context — the bag of dependencies every command receives.
//!
//! [`ExecutionContext`] is the entry point's dependency-injection
//! envelope: filesystem, datastore, config manager, runner, plus the
//! flags that scope a single invocation (`dry_run`, `force`, view/group
//! mode, …). Production wires up [`ExecutionContext::production`];
//! tests assemble fields directly.
use std::sync::Arc;
use crate::config::ConfigManager;
use crate::datastore::DataStore;
use crate::fs::Fs;
use crate::gates::HostFacts;
use crate::paths::Pather;
/// Everything the pipeline needs to execute.
pub struct ExecutionContext {
pub fs: Arc<dyn Fs>,
pub datastore: Arc<dyn DataStore>,
pub paths: Arc<dyn Pather>,
pub config_manager: Arc<ConfigManager>,
/// Pre-flight syntax checker for shell sources. Production wires
/// up [`SystemSyntaxChecker`](crate::shell::SystemSyntaxChecker)
/// (spawns real `bash`/`zsh -n`); tests inject a mock.
pub syntax_checker: Arc<dyn crate::shell::SyntaxChecker>,
/// Subprocess runner for advisory probes (homebrew-cask lookup,
/// macOS `mdls`/`mdfind`). Production reuses the same
/// [`ShellCommandRunner`](crate::datastore::ShellCommandRunner)
/// the datastore uses for handler-driven commands; tests inject a
/// mock that returns canned outputs without spawning processes.
/// See `docs/proposals/macos-paths.lex` §8.
pub command_runner: Arc<dyn crate::datastore::CommandRunner>,
pub dry_run: bool,
pub no_provision: bool,
pub provision_rerun: bool,
pub force: bool,
/// Opt-in drift detection for externals. When true, `status`
/// hashes each deployed external entry's content and compares
/// against the configured signature, surfacing any divergence as
/// a warning. Default `false` because hashing every deployed
/// external on every `status` invocation is not the right default
/// for big trees (oh-my-zsh, etc.).
pub check_drift: bool,
/// Opt-in unified-diff display for run-once handlers. When true,
/// `status` populates [`PackStatusResult::diffs`](crate::commands::PackStatusResult)
/// for any `install` / `homebrew` row whose recorded sentinel hash
/// no longer matches the current source AND whose snapshot sibling
/// is on disk. Default `false`; surfaced via the `--diff` flag on
/// `dodot status`.
pub show_diff: bool,
/// How pack-status output should render rows: `Full` keeps today's
/// per-file listing, `Short` collapses each pack to one summary
/// line. Consumed by every command that renders through the
/// `pack-status` template (`status`, `up`, `down`, `adopt`);
/// ignored by commands that emit `message` / `list` output.
pub view_mode: crate::commands::ViewMode,
/// How packs are ordered in pack-status output: `Name` (flat
/// alphabetical / discovery order) or `Status` (grouped under
/// Deployed / Pending / Error banners, followed by ignored rows). Consumed by
/// every command that renders through the `pack-status` template;
/// ignored by commands that emit `message` / `list` output.
pub group_mode: crate::commands::GroupMode,
/// When true, install-script execution streams raw stdout/stderr
/// to the user's terminal. The default (`false`) keeps output
/// quiet — only the `# status:` progress markers and the leading
/// comment block of each script are surfaced. Wired from the CLI
/// global `--verbose`/`--debug` flag.
pub verbose: bool,
/// Snapshot of the host's gate-relevant facts (os, arch, hostname,
/// username). Detected once per context so per-pack scanning and
/// matching avoid re-running `hostname(1)`/env reads. Constructed
/// by [`Self::production`]; tests build via `HostFacts::for_tests`.
pub host_facts: Arc<HostFacts>,
/// The init-script generation the calling shell exported
/// (`DODOT_INIT_GEN`), if any — signal 1 of the shell-hookup
/// ladder (`docs/proposals/shipped/shell-hookup.lex` §2.1). Snapshotted at
/// context construction by [`Self::production`] so activation
/// evaluation is a function of its inputs and tests can inject a
/// stamp instead of mutating process-global environment.
pub env_init_gen: Option<u64>,
/// Whether this invocation is attached to a terminal — the
/// *session* evidence that breaks the stampless tie in the
/// shell-hookup ladder (#279): a tty-attached process with no
/// `DODOT_INIT_GEN` is direct proof the session in front of the
/// user did not load dodot, whatever the heartbeat high-water mark
/// claims about past sessions. Snapshotted at context construction
/// (stderr `isatty`, the fd least likely to be redirected) so
/// evaluation stays a function of its inputs; `false` in tests
/// unless a test opts in. Consumed only by `status`
/// (`status::shell_hookup_notice`) — `up` and `install` measure
/// instead of inferring from the session.
pub tty: bool,
/// Whether this invocation may spawn a shell to *measure*
/// activation — signal 3 of the hookup ladder
/// (`docs/proposals/shipped/shell-hookup.lex` §3).
///
/// Defaults to
/// [`ProbePolicy::Never`](crate::shell::ProbePolicy::Never)
/// everywhere except [`Self::production`], so no test can spawn a
/// real shell by forgetting to opt out; probe tests opt *in*, with
/// a fabricated shell in `shell_env`. `status` never probes
/// whatever this says (spec §9) — it reads evidence only.
pub shell_probe: crate::shell::ProbePolicy,
/// The user's shell and `$ZDOTDIR`, snapshotted once.
///
/// One source of truth for two consumers that must agree: the
/// probe spawns `shell_env.shell`, and the rc ladder
/// ([`crate::shell::rc`]) resolves the file that same shell reads
/// — so a failed probe's diagnosis always names the rc file of the
/// shell that was actually measured. Default (both `None`) in
/// tests, which is why a test that never opts in cannot reach the
/// developer's real `~/.zshrc` either.
pub shell_env: crate::shell::ShellEnv,
}
impl ExecutionContext {
/// Create a default production context from a dotfiles root path.
///
/// Wires up the real filesystem, XDG paths, filesystem-backed
/// datastore with shell command runner, and clapfig config manager.
/// `verbose` controls whether install-script stdout/stderr is
/// streamed to the terminal; the field is also stored on the
/// returned context for any other consumer that cares. Callers
/// only need to override specific fields (e.g. `dry_run`).
pub fn production(dotfiles_root: &std::path::Path, verbose: bool) -> crate::Result<Self> {
let config_manager = Arc::new(ConfigManager::new(dotfiles_root)?);
// Honor `app_uses_library = false` by collapsing app_support_dir
// onto xdg_config_home — that's the "Linux-style ~/.config
// everywhere even on macOS" escape hatch from
// `docs/proposals/macos-paths.lex` §6.3 / §11.2.
//
// Soft-fail by design: a config-load failure here only blocks
// the `app_uses_library = false` override from being applied,
// not context construction itself. Real config errors (parse
// failures, missing required fields) bubble up the next time a
// command calls `config_manager.root_config()` — same surface,
// same error path, just without preempting Pather construction.
// If the read fails here we leave `app_support_dir` at the
// platform default and let the actual command surface the error.
let mut paths_builder = crate::paths::XdgPather::builder().dotfiles_root(dotfiles_root);
if let Ok(root_config) = config_manager.root_config() {
if !root_config.symlink.app_uses_library {
// Resolve XDG the way XdgPatherBuilder will, then pin
// app_support_dir at the same path. We can't read the
// builder's resolved xdg back out before build(), so
// duplicate the precedence here.
let home = std::env::var("HOME")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/tmp/dodot-unknown-home"));
let xdg = std::env::var("XDG_CONFIG_HOME")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| home.join(".config"));
paths_builder = paths_builder.app_support_dir(xdg);
}
}
let paths = Arc::new(paths_builder.build()?);
let fs: Arc<dyn Fs> = Arc::new(crate::fs::OsFs::new());
let runner: Arc<dyn crate::datastore::CommandRunner> =
Arc::new(crate::datastore::ShellCommandRunner::new(verbose));
let datastore: Arc<dyn DataStore> = Arc::new(crate::datastore::FilesystemDataStore::new(
fs.clone(),
paths.clone(),
runner.clone(),
));
Ok(Self {
fs,
datastore,
paths,
config_manager,
syntax_checker: Arc::new(crate::shell::SystemSyntaxChecker),
command_runner: runner,
dry_run: false,
no_provision: false,
provision_rerun: false,
force: false,
check_drift: false,
show_diff: false,
view_mode: crate::commands::ViewMode::default(),
group_mode: crate::commands::GroupMode::default(),
verbose,
host_facts: Arc::new(HostFacts::detect()),
env_init_gen: crate::shell::activation::read_env_stamp(),
tty: std::io::IsTerminal::is_terminal(&std::io::stderr()),
shell_probe: crate::shell::ProbePolicy::production(),
shell_env: crate::shell::ShellEnv::from_process(),
})
}
}