handoff-mcp-server 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
@@ -1,402 +0,0 @@
1
- use anyhow::{Context, Result};
2
- use serde_json::Value;
3
- use toml_edit::{DocumentMut, Item, Value as TomlValue};
4
-
5
- use super::resolve_project_dir;
6
- use crate::storage::config::{read_config, write_config};
7
- use crate::storage::ensure_handoff_exists;
8
-
9
- pub fn handle_get(arguments: &Value) -> Result<String> {
10
- let project_dir = resolve_project_dir(arguments)?;
11
-
12
- let handoff = ensure_handoff_exists(&project_dir)?;
13
- let config_path = handoff.join("config.toml");
14
-
15
- let raw = std::fs::read_to_string(&config_path)
16
- .with_context(|| format!("Failed to read config: {}", config_path.display()))?;
17
- let doc: DocumentMut = raw.parse().with_context(|| "Failed to parse config.toml")?;
18
-
19
- let toml_str = doc.to_string();
20
- let toml_value: toml::Value =
21
- toml::from_str(&toml_str).with_context(|| "Failed to deserialize config")?;
22
- let json_value =
23
- serde_json::to_value(&toml_value).with_context(|| "Failed to convert to JSON")?;
24
-
25
- serde_json::to_string_pretty(&json_value).context("Failed to format config")
26
- }
27
-
28
- pub fn handle_update(arguments: &Value) -> Result<String> {
29
- let project_dir = resolve_project_dir(arguments)?;
30
-
31
- let handoff = ensure_handoff_exists(&project_dir)?;
32
- let config_path = handoff.join("config.toml");
33
-
34
- let updates = arguments
35
- .get("updates")
36
- .ok_or_else(|| anyhow::anyhow!("'updates' parameter is required"))?;
37
-
38
- let updates = updates
39
- .as_object()
40
- .ok_or_else(|| anyhow::anyhow!("'updates' must be an object"))?;
41
-
42
- // Separate typed keys (settings/dashboard/project) from raw TOML keys
43
- let typed_keys = [
44
- "settings.history_limit",
45
- "settings.done_task_limit",
46
- "settings.auto_git_summary",
47
- "settings.require_estimate_hours",
48
- "settings.ai_estimate_multiplier",
49
- "settings.context_files",
50
- "settings.memory_enabled",
51
- "settings.memory_dup_threshold",
52
- "settings.memory_query_min_score",
53
- "settings.memory_query_relative_threshold",
54
- "settings.memory_query_limit",
55
- "settings.memory_stale_days",
56
- "settings.memory_injected_gc_days",
57
- "settings.timer_provider",
58
- "settings.timer_authority_ttl_secs",
59
- "settings.timer_idle_timeout_minutes",
60
- "dashboard.scan_dirs",
61
- "dashboard.exclude_patterns",
62
- "dashboard.max_depth",
63
- "project.name",
64
- "project.description",
65
- ];
66
-
67
- let mut has_typed = false;
68
- let mut has_raw = false;
69
-
70
- for key in updates.keys() {
71
- if typed_keys.contains(&key.as_str()) {
72
- has_typed = true;
73
- } else {
74
- has_raw = true;
75
- }
76
- }
77
-
78
- let mut applied = Vec::new();
79
-
80
- // Handle typed keys via existing Config struct
81
- if has_typed {
82
- let mut config = read_config(&config_path)?;
83
- for (key, value) in updates {
84
- match key.as_str() {
85
- "settings.history_limit" => {
86
- if let Some(n) = value.as_u64() {
87
- config.settings.history_limit = n as u32;
88
- applied.push(format!("settings.history_limit = {n}"));
89
- }
90
- }
91
- "settings.done_task_limit" => {
92
- if let Some(n) = value.as_u64() {
93
- config.settings.done_task_limit = n as u32;
94
- applied.push(format!("settings.done_task_limit = {n}"));
95
- }
96
- }
97
- "settings.auto_git_summary" => {
98
- if let Some(b) = value.as_bool() {
99
- config.settings.auto_git_summary = b;
100
- applied.push(format!("settings.auto_git_summary = {b}"));
101
- }
102
- }
103
- "settings.require_estimate_hours" => {
104
- if let Some(b) = value.as_bool() {
105
- config.settings.require_estimate_hours = b;
106
- applied.push(format!("settings.require_estimate_hours = {b}"));
107
- }
108
- }
109
- "settings.ai_estimate_multiplier" => {
110
- if let Some(n) = value.as_f64() {
111
- if n < 0.0 {
112
- anyhow::bail!("settings.ai_estimate_multiplier must be >= 0");
113
- }
114
- config.settings.ai_estimate_multiplier = n;
115
- applied.push(format!("settings.ai_estimate_multiplier = {n}"));
116
- }
117
- }
118
- "settings.context_files" => {
119
- if let Some(arr) = value.as_array() {
120
- config.settings.context_files = arr
121
- .iter()
122
- .filter_map(|v| v.as_str().map(String::from))
123
- .collect();
124
- applied.push(format!(
125
- "settings.context_files = {:?}",
126
- config.settings.context_files
127
- ));
128
- }
129
- }
130
- "settings.memory_enabled" => {
131
- let Some(b) = value.as_bool() else {
132
- anyhow::bail!("settings.memory_enabled must be a boolean");
133
- };
134
- config.settings.memory_enabled = b;
135
- applied.push(format!("settings.memory_enabled = {b}"));
136
- }
137
- "settings.memory_dup_threshold" => {
138
- let Some(n) = value.as_f64() else {
139
- anyhow::bail!("settings.memory_dup_threshold must be a number");
140
- };
141
- if !(0.0..=1.0).contains(&n) {
142
- anyhow::bail!("settings.memory_dup_threshold must be between 0 and 1");
143
- }
144
- config.settings.memory_dup_threshold = n;
145
- applied.push(format!("settings.memory_dup_threshold = {n}"));
146
- }
147
- "settings.memory_query_min_score" => {
148
- let Some(n) = value.as_f64() else {
149
- anyhow::bail!("settings.memory_query_min_score must be a number");
150
- };
151
- if n < 0.0 {
152
- anyhow::bail!("settings.memory_query_min_score must be >= 0");
153
- }
154
- config.settings.memory_query_min_score = n;
155
- applied.push(format!("settings.memory_query_min_score = {n}"));
156
- }
157
- "settings.memory_query_relative_threshold" => {
158
- let Some(n) = value.as_f64() else {
159
- anyhow::bail!("settings.memory_query_relative_threshold must be a number");
160
- };
161
- if !(0.0..=1.0).contains(&n) {
162
- anyhow::bail!(
163
- "settings.memory_query_relative_threshold must be between 0 and 1"
164
- );
165
- }
166
- config.settings.memory_query_relative_threshold = n;
167
- applied.push(format!("settings.memory_query_relative_threshold = {n}"));
168
- }
169
- "settings.memory_query_limit" => {
170
- let Some(n) = value.as_u64() else {
171
- anyhow::bail!("settings.memory_query_limit must be a non-negative integer");
172
- };
173
- if n == 0 {
174
- anyhow::bail!("settings.memory_query_limit must be >= 1");
175
- }
176
- config.settings.memory_query_limit = n as u32;
177
- applied.push(format!("settings.memory_query_limit = {n}"));
178
- }
179
- "settings.memory_stale_days" => {
180
- let Some(n) = value.as_i64() else {
181
- anyhow::bail!("settings.memory_stale_days must be an integer");
182
- };
183
- if n < 0 {
184
- anyhow::bail!("settings.memory_stale_days must be >= 0");
185
- }
186
- config.settings.memory_stale_days = n;
187
- applied.push(format!("settings.memory_stale_days = {n}"));
188
- }
189
- "settings.memory_injected_gc_days" => {
190
- let Some(n) = value.as_i64() else {
191
- anyhow::bail!("settings.memory_injected_gc_days must be an integer");
192
- };
193
- if n < 0 {
194
- anyhow::bail!("settings.memory_injected_gc_days must be >= 0");
195
- }
196
- config.settings.memory_injected_gc_days = n;
197
- applied.push(format!("settings.memory_injected_gc_days = {n}"));
198
- }
199
- "settings.timer_provider" => {
200
- let Some(s) = value.as_str() else {
201
- anyhow::bail!("settings.timer_provider must be a string");
202
- };
203
- let valid = ["auto", "vscode", "mcp", "off"];
204
- if !valid.contains(&s) {
205
- anyhow::bail!(
206
- "settings.timer_provider must be one of: {}",
207
- valid.join(", ")
208
- );
209
- }
210
- config.settings.timer_provider = s.to_string();
211
- applied.push(format!("settings.timer_provider = {s}"));
212
- }
213
- "settings.timer_authority_ttl_secs" => {
214
- let Some(n) = value.as_u64() else {
215
- anyhow::bail!(
216
- "settings.timer_authority_ttl_secs must be a positive integer"
217
- );
218
- };
219
- if n == 0 {
220
- anyhow::bail!("settings.timer_authority_ttl_secs must be >= 1");
221
- }
222
- config.settings.timer_authority_ttl_secs = n;
223
- applied.push(format!("settings.timer_authority_ttl_secs = {n}"));
224
- }
225
- "settings.timer_idle_timeout_minutes" => {
226
- let Some(n) = value.as_u64() else {
227
- anyhow::bail!(
228
- "settings.timer_idle_timeout_minutes must be a non-negative integer"
229
- );
230
- };
231
- config.settings.timer_idle_timeout_minutes = n;
232
- applied.push(format!("settings.timer_idle_timeout_minutes = {n}"));
233
- }
234
- "dashboard.scan_dirs" => {
235
- if let Some(arr) = value.as_array() {
236
- config.dashboard.scan_dirs = arr
237
- .iter()
238
- .filter_map(|v| v.as_str().map(String::from))
239
- .collect();
240
- applied.push(format!(
241
- "dashboard.scan_dirs = {:?}",
242
- config.dashboard.scan_dirs
243
- ));
244
- }
245
- }
246
- "dashboard.exclude_patterns" => {
247
- if let Some(arr) = value.as_array() {
248
- config.dashboard.exclude_patterns = arr
249
- .iter()
250
- .filter_map(|v| v.as_str().map(String::from))
251
- .collect();
252
- applied.push(format!(
253
- "dashboard.exclude_patterns = {:?}",
254
- config.dashboard.exclude_patterns
255
- ));
256
- }
257
- }
258
- "dashboard.max_depth" => {
259
- let Some(n) = value.as_u64() else {
260
- anyhow::bail!("dashboard.max_depth must be a non-negative integer");
261
- };
262
- if n == 0 {
263
- anyhow::bail!("dashboard.max_depth must be >= 1");
264
- }
265
- config.dashboard.max_depth = n as usize;
266
- applied.push(format!("dashboard.max_depth = {n}"));
267
- }
268
- "project.name" => {
269
- if let Some(s) = value.as_str() {
270
- config.project.name = s.to_string();
271
- applied.push(format!("project.name = {s}"));
272
- }
273
- }
274
- "project.description" => {
275
- if let Some(s) = value.as_str() {
276
- config.project.description = Some(s.to_string());
277
- applied.push(format!("project.description = {s}"));
278
- }
279
- }
280
- _ => {}
281
- }
282
- }
283
- write_config(&config_path, &config)?;
284
- }
285
-
286
- // Handle raw TOML keys (calendar, assignees, effort_budget, gantt_view, etc.)
287
- if has_raw {
288
- let raw = std::fs::read_to_string(&config_path)
289
- .with_context(|| format!("Failed to read config: {}", config_path.display()))?;
290
- let mut doc: DocumentMut = raw
291
- .parse()
292
- .with_context(|| "Failed to parse config.toml for raw update")?;
293
-
294
- for (key, value) in updates {
295
- if typed_keys.contains(&key.as_str()) {
296
- continue;
297
- }
298
-
299
- let parts: Vec<&str> = key.split('.').collect();
300
- if parts.is_empty() {
301
- applied.push(format!("{key}: invalid key"));
302
- continue;
303
- }
304
-
305
- match set_toml_value(&mut doc, &parts, value) {
306
- Ok(()) => applied.push(format!("{key} = {value}")),
307
- Err(e) => applied.push(format!("{key}: error ({e})")),
308
- }
309
- }
310
-
311
- crate::storage::atomic_write(&config_path, doc.to_string().as_bytes())
312
- .with_context(|| format!("Failed to write config: {}", config_path.display()))?;
313
- }
314
-
315
- if applied.is_empty() {
316
- Ok("No updates applied".to_string())
317
- } else {
318
- Ok(format!("Updated config:\n{}", applied.join("\n")))
319
- }
320
- }
321
-
322
- fn set_toml_value(doc: &mut DocumentMut, parts: &[&str], json_val: &Value) -> Result<()> {
323
- let toml_val = json_to_toml_value(json_val)?;
324
-
325
- match parts.len() {
326
- 1 => {
327
- doc[parts[0]] = Item::Value(toml_val);
328
- }
329
- 2 => {
330
- if !doc.contains_table(parts[0]) {
331
- doc[parts[0]] = Item::Table(toml_edit::Table::new());
332
- }
333
- doc[parts[0]][parts[1]] = Item::Value(toml_val);
334
- }
335
- 3 => {
336
- if !doc.contains_table(parts[0]) {
337
- doc[parts[0]] = Item::Table(toml_edit::Table::new());
338
- }
339
- let table = doc[parts[0]]
340
- .as_table_mut()
341
- .ok_or_else(|| anyhow::anyhow!("{} is not a table", parts[0]))?;
342
- if !table.contains_table(parts[1]) {
343
- table[parts[1]] = Item::Table(toml_edit::Table::new());
344
- }
345
- table[parts[1]][parts[2]] = Item::Value(toml_val);
346
- }
347
- 4 => {
348
- if !doc.contains_table(parts[0]) {
349
- doc[parts[0]] = Item::Table(toml_edit::Table::new());
350
- }
351
- let t0 = doc[parts[0]]
352
- .as_table_mut()
353
- .ok_or_else(|| anyhow::anyhow!("{} is not a table", parts[0]))?;
354
- if !t0.contains_table(parts[1]) {
355
- t0[parts[1]] = Item::Table(toml_edit::Table::new());
356
- }
357
- let t1 = t0[parts[1]]
358
- .as_table_mut()
359
- .ok_or_else(|| anyhow::anyhow!("{}.{} is not a table", parts[0], parts[1]))?;
360
- if !t1.contains_table(parts[2]) {
361
- t1[parts[2]] = Item::Table(toml_edit::Table::new());
362
- }
363
- t1[parts[2]][parts[3]] = Item::Value(toml_val);
364
- }
365
- _ => {
366
- anyhow::bail!("Key depth > 4 not supported");
367
- }
368
- }
369
-
370
- Ok(())
371
- }
372
-
373
- fn json_to_toml_value(val: &Value) -> Result<TomlValue> {
374
- match val {
375
- Value::String(s) => Ok(TomlValue::from(s.as_str())),
376
- Value::Number(n) => {
377
- if let Some(i) = n.as_i64() {
378
- Ok(TomlValue::from(i))
379
- } else if let Some(f) = n.as_f64() {
380
- Ok(TomlValue::from(f))
381
- } else {
382
- anyhow::bail!("Unsupported number: {n}")
383
- }
384
- }
385
- Value::Bool(b) => Ok(TomlValue::from(*b)),
386
- Value::Array(arr) => {
387
- let mut toml_arr = toml_edit::Array::new();
388
- for item in arr {
389
- toml_arr.push(json_to_toml_value(item)?);
390
- }
391
- Ok(TomlValue::Array(toml_arr))
392
- }
393
- Value::Object(obj) => {
394
- let mut inline = toml_edit::InlineTable::new();
395
- for (k, v) in obj {
396
- inline.insert(k, json_to_toml_value(v)?);
397
- }
398
- Ok(TomlValue::InlineTable(inline))
399
- }
400
- Value::Null => Ok(TomlValue::from("")),
401
- }
402
- }
@@ -1,183 +0,0 @@
1
- //! Shared helpers for config.toml CRUD handlers (assignees, milestones,
2
- //! calendar, labels, project start). All mutate the raw TOML document via
3
- //! `toml_edit` so that comments and unrelated keys are preserved, then write it
4
- //! back atomically.
5
-
6
- use std::path::{Path, PathBuf};
7
-
8
- use anyhow::{Context, Result};
9
- use serde_json::Value;
10
- use toml_edit::{Array, DocumentMut, Item, Value as TomlValue};
11
-
12
- use super::resolve_project_dir;
13
- use crate::storage::ensure_handoff_exists;
14
-
15
- /// Resolve the project dir, ensure `.handoff/` exists, and return the
16
- /// config.toml path.
17
- pub fn config_path(arguments: &Value) -> Result<PathBuf> {
18
- let project_dir = resolve_project_dir(arguments)?;
19
- let handoff = ensure_handoff_exists(&project_dir)?;
20
- Ok(handoff.join("config.toml"))
21
- }
22
-
23
- /// Parse config.toml into a mutable document.
24
- pub fn load_doc(path: &Path) -> Result<DocumentMut> {
25
- let raw = std::fs::read_to_string(path)
26
- .with_context(|| format!("Failed to read config: {}", path.display()))?;
27
- raw.parse::<DocumentMut>()
28
- .with_context(|| format!("Failed to parse config: {}", path.display()))
29
- }
30
-
31
- /// Serialize and atomically write the document back to disk.
32
- pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<()> {
33
- crate::storage::atomic_write(path, doc.to_string().as_bytes())
34
- .with_context(|| format!("Failed to write config: {}", path.display()))
35
- }
36
-
37
- /// Required string argument or a descriptive error.
38
- pub fn require_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str> {
39
- arguments
40
- .get(key)
41
- .and_then(|v| v.as_str())
42
- .ok_or_else(|| anyhow::anyhow!("'{key}' is required"))
43
- }
44
-
45
- /// Set `table[field]` to a string if the JSON arg is present; if it is JSON
46
- /// null, remove the key (allows clearing a value).
47
- pub fn set_opt_str(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
48
- match arg {
49
- Some(Value::Null) => {
50
- table.remove(field);
51
- }
52
- Some(v) => {
53
- if let Some(s) = v.as_str() {
54
- table[field] = Item::Value(TomlValue::from(s));
55
- }
56
- }
57
- None => {}
58
- }
59
- }
60
-
61
- /// Set `table[field]` to a number if present; null removes it.
62
- pub fn set_opt_f64(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
63
- match arg {
64
- Some(Value::Null) => {
65
- table.remove(field);
66
- }
67
- Some(v) => {
68
- if let Some(n) = v.as_f64() {
69
- table[field] = Item::Value(TomlValue::from(n));
70
- }
71
- }
72
- None => {}
73
- }
74
- }
75
-
76
- /// Set `table[field]` to a bool if present; null removes it.
77
- pub fn set_opt_bool(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
78
- match arg {
79
- Some(Value::Null) => {
80
- table.remove(field);
81
- }
82
- Some(v) => {
83
- if let Some(b) = v.as_bool() {
84
- table[field] = Item::Value(TomlValue::from(b));
85
- }
86
- }
87
- None => {}
88
- }
89
- }
90
-
91
- /// Set `table[field]` to a TOML array of strings if the JSON arg is an array;
92
- /// null removes it.
93
- pub fn set_string_array(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
94
- match arg {
95
- Some(Value::Null) => {
96
- table.remove(field);
97
- }
98
- Some(Value::Array(items)) => {
99
- let mut arr = Array::new();
100
- for it in items {
101
- if let Some(s) = it.as_str() {
102
- arr.push(s);
103
- }
104
- }
105
- table[field] = Item::Value(TomlValue::Array(arr));
106
- }
107
- _ => {}
108
- }
109
- }
110
-
111
- /// Set `table[field]` to a TOML array preserving integer-or-string items
112
- /// (used for `closed_weekdays`, which may be numbers or weekday names); null removes it.
113
- pub fn set_mixed_array(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
114
- match arg {
115
- Some(Value::Null) => {
116
- table.remove(field);
117
- }
118
- Some(Value::Array(items)) => {
119
- let mut arr = Array::new();
120
- for it in items {
121
- if let Some(i) = it.as_i64() {
122
- arr.push(i);
123
- } else if let Some(s) = it.as_str() {
124
- arr.push(s);
125
- }
126
- }
127
- table[field] = Item::Value(TomlValue::Array(arr));
128
- }
129
- _ => {}
130
- }
131
- }
132
-
133
- /// Replace a nested `[parent.<key>.<field>]`-style map of `{name: hours}` with
134
- /// the provided JSON object (e.g. `day_hours`). null removes the whole sub-table.
135
- pub fn set_f64_map(table: &mut toml_edit::Table, field: &str, arg: Option<&Value>) {
136
- match arg {
137
- Some(Value::Null) => {
138
- table.remove(field);
139
- }
140
- Some(Value::Object(map)) => {
141
- let mut sub = toml_edit::Table::new();
142
- sub.set_implicit(false);
143
- for (k, v) in map {
144
- if let Some(n) = v.as_f64() {
145
- sub[k] = Item::Value(TomlValue::from(n));
146
- }
147
- }
148
- table[field] = Item::Table(sub);
149
- }
150
- _ => {}
151
- }
152
- }
153
-
154
- /// Get a mutable reference to `doc[section]` as a table, creating it if absent.
155
- pub fn ensure_table<'a>(
156
- doc: &'a mut DocumentMut,
157
- section: &str,
158
- ) -> Result<&'a mut toml_edit::Table> {
159
- if !doc.contains_table(section) {
160
- doc[section] = Item::Table(toml_edit::Table::new());
161
- }
162
- doc[section]
163
- .as_table_mut()
164
- .ok_or_else(|| anyhow::anyhow!("[{section}] exists but is not a table"))
165
- }
166
-
167
- /// Get a mutable reference to `doc[section][key]` as a table, creating both if
168
- /// absent. Marks the parent as a dotted/implicit table so it serializes as
169
- /// `[section.key]`.
170
- pub fn ensure_subtable<'a>(
171
- doc: &'a mut DocumentMut,
172
- section: &str,
173
- key: &str,
174
- ) -> Result<&'a mut toml_edit::Table> {
175
- let parent = ensure_table(doc, section)?;
176
- parent.set_implicit(true);
177
- if !parent.contains_table(key) {
178
- parent[key] = Item::Table(toml_edit::Table::new());
179
- }
180
- parent[key]
181
- .as_table_mut()
182
- .ok_or_else(|| anyhow::anyhow!("[{section}.{key}] exists but is not a table"))
183
- }