Skip to main content

kaizen/shell/
init.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! `kaizen init` — idempotent workspace setup.
3
4use crate::ipc::{CaptureComponentStatus, CaptureStatus};
5use anyhow::Result;
6use std::fmt::Write;
7use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10const CONFIG_TOML: &str = r#"[kaizen]
11
12# Optional sync (usually override secrets in ~/.kaizen/config.toml):
13# [sync]
14# endpoint = "https://ingest.example.com"
15# team_token = "Bearer-token-from-server"
16# team_id = "your-team"
17# events_per_batch_max = 500
18# max_body_bytes = 1000000
19# flush_interval_ms = 10000
20# sample_rate = 1.0
21"#;
22const KAIZEN_RETRO_SKILL: &str = include_str!("../../assets/kaizen-retro-SKILL.md");
23const KAIZEN_EVAL_SKILL: &str = include_str!("../../assets/kaizen-eval-SKILL.md");
24
25const CURSOR_HOOK_EVENTS: &[&str] = &[
26    "SessionStart",
27    "UserPromptSubmit",
28    "PreToolUse",
29    "PostToolUse",
30    "Stop",
31];
32const CLAUDE_HOOK_EVENTS: &[&str] = &[
33    "SessionStart",
34    "UserPromptSubmit",
35    "PreToolUse",
36    "PostToolUse",
37    "Stop",
38];
39
40#[derive(Clone, Copy, Debug, Default)]
41pub struct InitOptions {
42    pub deep: bool,
43    pub start_capture: bool,
44}
45
46fn ts_ms() -> u64 {
47    SystemTime::now()
48        .duration_since(UNIX_EPOCH)
49        .unwrap_or_default()
50        .as_millis() as u64
51}
52
53fn backup_path(ws: &Path, filename: &str) -> Result<PathBuf> {
54    let relative = PathBuf::from("backup").join(format!("{}.{}.bak", filename, ts_ms()));
55    crate::core::paths::project_file_for_write(ws, &relative)
56}
57
58fn ensure_config(out: &mut String, ws: &Path) -> Result<()> {
59    let path = crate::core::paths::project_file_for_write(ws, Path::new("config.toml"))?;
60    if path.exists() {
61        writeln!(out, "  skipped  config.toml (project data dir)").unwrap();
62        return Ok(());
63    }
64    let mut file = crate::core::safe_fs::create_new(&path)?;
65    std::io::Write::write_all(&mut file, CONFIG_TOML.as_bytes())?;
66    writeln!(out, "  created  {}", path.display()).unwrap();
67    Ok(())
68}
69
70/// Hook command string written to `.cursor/hooks.json`.
71pub const KAIZEN_CURSOR_HOOK_CMD: &str = "kaizen ingest hook --source cursor";
72pub const KAIZEN_OPENCLAW_HOOK_CMD: &str = "kaizen ingest hook --source openclaw";
73const KAIZEN_OPENCLAW_SPAWN_ARGS: &str = r#""ingest", "hook", "--source", "openclaw""#;
74/// Hook command string written to `.claude/settings.json`.
75pub const KAIZEN_CLAUDE_HOOK_CMD: &str = "kaizen ingest hook --source claude";
76
77/// `true` if every Cursor hook event points at the kaizen ingest command.
78fn cursor_hooks_done(root: &serde_json::Value) -> bool {
79    CURSOR_HOOK_EVENTS
80        .iter()
81        .all(|event| cursor_hook_exists(root, event))
82}
83
84fn cursor_hook_exists(root: &serde_json::Value, event: &str) -> bool {
85    if let Some(arr) = root
86        .pointer(&format!("/hooks/{event}"))
87        .and_then(|v| v.as_array())
88    {
89        return arr
90            .iter()
91            .any(|v| v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD));
92    }
93    if let Some(arr) = root.as_array() {
94        return arr.iter().any(|v| {
95            v.get("matcher").and_then(|m| m.as_str()) == Some(event)
96                && v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD)
97        });
98    }
99    false
100}
101
102fn patch_cursor_hooks(out: &mut String, ws: &Path) -> Result<()> {
103    let Some(cursor_dir) = cursor_user_dir() else {
104        writeln!(out, "  skipped  ~/.cursor/hooks.json (HOME unset)").unwrap();
105        return Ok(());
106    };
107    let path = cursor_dir.join("hooks.json");
108    if !path.exists() {
109        std::fs::create_dir_all(path.parent().unwrap())?;
110        let mut obj = serde_json::Map::new();
111        let mut hooks = serde_json::Map::new();
112        for event in CURSOR_HOOK_EVENTS {
113            hooks.insert(
114                (*event).to_string(),
115                serde_json::json!([{"command": KAIZEN_CURSOR_HOOK_CMD}]),
116            );
117        }
118        obj.insert("hooks".to_string(), serde_json::Value::Object(hooks));
119        write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
120        writeln!(out, "  created  ~/.cursor/hooks.json").unwrap();
121        return Ok(());
122    }
123    let raw = std::fs::read_to_string(&path)?;
124    let mut root: serde_json::Value = match serde_json::from_str(&raw) {
125        Ok(v) => v,
126        Err(e) => {
127            writeln!(out, "  error  ~/.cursor/hooks.json: {e}").unwrap();
128            anyhow::bail!("malformed ~/.cursor/hooks.json: {e}");
129        }
130    };
131    if cursor_hooks_done(&root) {
132        writeln!(out, "  skipped  ~/.cursor/hooks.json").unwrap();
133        return Ok(());
134    }
135    let bak = backup_path(ws, "cursor_hooks")?;
136    std::fs::copy(&path, &bak)?;
137    if let Some(obj) = root.pointer_mut("/hooks").and_then(|v| v.as_object_mut()) {
138        for event in CURSOR_HOOK_EVENTS {
139            let arr = obj
140                .entry((*event).to_string())
141                .or_insert_with(|| serde_json::json!([]));
142            if let Some(hooks) = arr.as_array_mut()
143                && !hooks.iter().any(|v| {
144                    v.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CURSOR_HOOK_CMD)
145                })
146            {
147                hooks.push(serde_json::json!({"command": KAIZEN_CURSOR_HOOK_CMD}));
148            }
149        }
150    } else if let Some(arr) = root.as_array_mut() {
151        for event in CURSOR_HOOK_EVENTS {
152            if !cursor_hook_exists(&serde_json::Value::Array(arr.clone()), event) {
153                arr.push(serde_json::json!({"matcher": event, "command": KAIZEN_CURSOR_HOOK_CMD}));
154            }
155        }
156    }
157    write_atomic(&path, &serde_json::to_string_pretty(&root)?)?;
158    writeln!(
159        out,
160        "  patched  ~/.cursor/hooks.json  (+session/tool hooks)"
161    )
162    .unwrap();
163    Ok(())
164}
165
166fn entry_has_kaizen_cmd(entry: &serde_json::Value) -> bool {
167    if entry.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CLAUDE_HOOK_CMD) {
168        return true;
169    }
170    entry
171        .get("hooks")
172        .and_then(|v| v.as_array())
173        .is_some_and(|inner| {
174            inner
175                .iter()
176                .any(|h| h.get("command").and_then(|c| c.as_str()) == Some(KAIZEN_CLAUDE_HOOK_CMD))
177        })
178}
179
180fn patch_claude_settings(out: &mut String, ws: &Path) -> Result<()> {
181    let Some(claude_dir) = claude_user_dir() else {
182        writeln!(out, "  skipped  ~/.claude/settings.json (HOME unset)").unwrap();
183        return Ok(());
184    };
185    let path = claude_dir.join("settings.json");
186    if !path.exists() {
187        std::fs::create_dir_all(path.parent().unwrap())?;
188        let mut obj = serde_json::Map::new();
189        let mut hooks = serde_json::Map::new();
190        for event in CLAUDE_HOOK_EVENTS {
191            hooks.insert(
192                (*event).to_string(),
193                serde_json::json!([
194                    {"hooks": [{"type": "command", "command": KAIZEN_CLAUDE_HOOK_CMD}]}
195                ]),
196            );
197        }
198        obj.insert("hooks".to_string(), serde_json::Value::Object(hooks));
199        write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
200        writeln!(out, "  created  ~/.claude/settings.json").unwrap();
201        return Ok(());
202    }
203    let raw = std::fs::read_to_string(&path)?;
204    let mut obj: serde_json::Map<String, serde_json::Value> = match serde_json::from_str(&raw) {
205        Ok(v) => v,
206        Err(e) => {
207            writeln!(out, "  error  ~/.claude/settings.json: {e}").unwrap();
208            anyhow::bail!("malformed ~/.claude/settings.json: {e}");
209        }
210    };
211    let hooks = obj.entry("hooks").or_insert_with(|| serde_json::json!({}));
212    let hooks_obj = hooks.as_object_mut().unwrap();
213    let mut changed = false;
214    for event in CLAUDE_HOOK_EVENTS {
215        let arr = hooks_obj
216            .entry((*event).to_string())
217            .or_insert_with(|| serde_json::json!([]));
218        let Some(entries) = arr.as_array_mut() else {
219            continue;
220        };
221        // Migrate any bare {command,type} entries missing the `hooks` wrapper.
222        for entry in entries.iter_mut() {
223            if entry.get("hooks").is_some() {
224                continue;
225            }
226            if let Some(obj) = entry.as_object()
227                && obj.contains_key("command")
228            {
229                let inner = entry.clone();
230                *entry = serde_json::json!({ "hooks": [inner] });
231                changed = true;
232            }
233        }
234        if !entries.iter().any(entry_has_kaizen_cmd) {
235            entries.push(serde_json::json!({
236                "hooks": [
237                    {"type": "command", "command": KAIZEN_CLAUDE_HOOK_CMD}
238                ]
239            }));
240            changed = true;
241        }
242    }
243    if !changed {
244        writeln!(
245            out,
246            "  skipped  ~/.claude/settings.json  (already configured)"
247        )
248        .unwrap();
249        return Ok(());
250    }
251    let bak = backup_path(ws, "claude_settings")?;
252    std::fs::copy(&path, &bak)?;
253    write_atomic(&path, &serde_json::to_string_pretty(&obj)?)?;
254    writeln!(
255        out,
256        "  patched  ~/.claude/settings.json  (+session/tool hooks)"
257    )
258    .unwrap();
259    Ok(())
260}
261
262/// Read-only: `~/.cursor/hooks.json` missing, valid JSON with full kaizen wiring, or not.
263pub fn cursor_kaizen_hook_wiring(ws: &Path) -> Result<Option<bool>, String> {
264    let _ = ws;
265    let Some(cursor_dir) = cursor_user_dir() else {
266        return Ok(None);
267    };
268    let path = cursor_dir.join("hooks.json");
269    if !path.exists() {
270        return Ok(None);
271    }
272    let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
273    let root: serde_json::Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
274    Ok(Some(cursor_hooks_done(&root)))
275}
276
277/// Read-only: `~/.claude/settings.json` hooks all reference kaizen (same as post-patch `entry_has_kaizen_cmd`).
278pub fn claude_kaizen_hook_wiring(ws: &Path) -> Result<Option<bool>, String> {
279    let _ = ws;
280    let Some(claude_dir) = claude_user_dir() else {
281        return Ok(None);
282    };
283    let path = claude_dir.join("settings.json");
284    if !path.exists() {
285        return Ok(None);
286    }
287    let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
288    let obj: serde_json::Map<String, serde_json::Value> =
289        serde_json::from_str(&raw).map_err(|e| e.to_string())?;
290    let Some(hooks) = obj.get("hooks").and_then(|v| v.as_object()) else {
291        return Ok(Some(false));
292    };
293    for event in CLAUDE_HOOK_EVENTS {
294        let Some(arr) = hooks.get(*event).and_then(|v| v.as_array()) else {
295            return Ok(Some(false));
296        };
297        if !arr.iter().any(entry_has_kaizen_cmd) {
298            return Ok(Some(false));
299        }
300    }
301    Ok(Some(true))
302}
303
304/// Returns any workspace-local files that still contain kaizen wiring (legacy from pre-global init).
305pub fn detect_legacy_wiring(ws: &Path) -> Vec<PathBuf> {
306    let mut found = Vec::new();
307    let cursor_local = ws.join(".cursor/hooks.json");
308    if cursor_local.exists()
309        && let Ok(raw) = std::fs::read_to_string(&cursor_local)
310        && raw.contains(KAIZEN_CURSOR_HOOK_CMD)
311    {
312        found.push(cursor_local);
313    }
314    let claude_local = ws.join(".claude/settings.json");
315    if claude_local.exists()
316        && let Ok(raw) = std::fs::read_to_string(&claude_local)
317        && raw.contains(KAIZEN_CLAUDE_HOOK_CMD)
318    {
319        found.push(claude_local);
320    }
321    found
322}
323
324fn write_eval_skill(out: &mut String, ws: &Path) -> Result<()> {
325    let Some(cursor_dir) = cursor_user_dir() else {
326        writeln!(
327            out,
328            "  skipped  ~/.cursor/skills/kaizen-eval/SKILL.md (HOME unset)"
329        )
330        .unwrap();
331        return Ok(());
332    };
333    let path = cursor_dir.join("skills/kaizen-eval/SKILL.md");
334    let _ = ws;
335    std::fs::create_dir_all(path.parent().unwrap())?;
336    if path.exists() {
337        let existing = std::fs::read_to_string(&path)?;
338        if !existing.contains("placeholder") && !existing.trim().is_empty() {
339            writeln!(out, "  skipped  ~/.cursor/skills/kaizen-eval/SKILL.md").unwrap();
340            return Ok(());
341        }
342    }
343    std::fs::write(&path, KAIZEN_EVAL_SKILL)?;
344    writeln!(out, "  wrote  ~/.cursor/skills/kaizen-eval/SKILL.md").unwrap();
345    Ok(())
346}
347
348fn write_skill(out: &mut String, ws: &Path) -> Result<()> {
349    let Some(cursor_dir) = cursor_user_dir() else {
350        writeln!(
351            out,
352            "  skipped  ~/.cursor/skills/kaizen-retro/SKILL.md (HOME unset)"
353        )
354        .unwrap();
355        return Ok(());
356    };
357    let path = cursor_dir.join("skills/kaizen-retro/SKILL.md");
358    let _ = ws;
359    std::fs::create_dir_all(path.parent().unwrap())?;
360    if path.exists() {
361        let existing = std::fs::read_to_string(&path)?;
362        if !existing.contains("placeholder") && !existing.trim().is_empty() {
363            writeln!(out, "  skipped  ~/.cursor/skills/kaizen-retro/SKILL.md").unwrap();
364            return Ok(());
365        }
366    }
367    std::fs::write(&path, KAIZEN_RETRO_SKILL)?;
368    writeln!(out, "  wrote  ~/.cursor/skills/kaizen-retro/SKILL.md").unwrap();
369    Ok(())
370}
371
372const OPENCLAW_HOOK_EVENTS: &[&str] = &[
373    "message:received",
374    "message:sent",
375    "command:new",
376    "command:reset",
377    "command:stop",
378    "session:compact:before",
379    "session:compact:after",
380    "session:patch",
381];
382
383const OPENCLAW_HANDLER_TS: &str = r#"import { spawn } from "child_process";
384
385export async function handler(event: Record<string, unknown>) {
386  const payload = JSON.stringify({
387    event: event["type"] ?? event["event"],
388    session_id: event["sessionId"] ?? event["session_id"] ?? "",
389    timestamp_ms: typeof event["timestamp"] === "number" ? event["timestamp"] : Date.now(),
390    ...event,
391  });
392  const child = spawn("kaizen", ["ingest", "hook", "--source", "openclaw"], {
393    stdio: ["pipe", "ignore", "ignore"],
394  });
395  child.stdin?.write(payload + "\n");
396  child.stdin?.end();
397}
398"#;
399
400const OPENCLAW_HOOK_MD: &str = "# kaizen-events\n\nCaptures OpenClaw sessions for kaizen.\n";
401
402fn cursor_user_dir() -> Option<PathBuf> {
403    std::env::var("HOME")
404        .ok()
405        .map(|h| PathBuf::from(h).join(".cursor"))
406}
407
408fn claude_user_dir() -> Option<PathBuf> {
409    std::env::var("HOME")
410        .ok()
411        .map(|h| PathBuf::from(h).join(".claude"))
412}
413
414fn write_atomic(path: &Path, content: &str) -> Result<()> {
415    let mut tmp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
416    std::io::Write::write_all(&mut tmp, content.as_bytes())?;
417    tmp.persist(path)?;
418    Ok(())
419}
420
421fn openclaw_hooks_dir() -> Option<PathBuf> {
422    std::env::var("HOME")
423        .ok()
424        .map(|h| PathBuf::from(h).join(".openclaw/hooks/kaizen-events"))
425}
426
427/// Write (or idempotently skip) the OpenClaw TS hook handler.
428///
429/// Backs up any pre-existing `handler.ts` that does not already reference kaizen.
430pub fn patch_openclaw_handlers(out: &mut String, ws: &Path) -> Result<()> {
431    let Some(hook_dir) = openclaw_hooks_dir() else {
432        writeln!(
433            out,
434            "  skipped  ~/.openclaw/hooks/kaizen-events (HOME unset)"
435        )
436        .unwrap();
437        return Ok(());
438    };
439    let handler_path = hook_dir.join("handler.ts");
440    if handler_path.exists() {
441        let existing = std::fs::read_to_string(&handler_path)?;
442        if openclaw_handler_contains_kaizen(&existing) {
443            writeln!(out, "  skipped  ~/.openclaw/hooks/kaizen-events/handler.ts").unwrap();
444            return Ok(());
445        }
446        let bak = backup_path(ws, "openclaw_hook")?;
447        std::fs::copy(&handler_path, &bak)?;
448    }
449    std::fs::create_dir_all(&hook_dir)?;
450    std::fs::write(&handler_path, OPENCLAW_HANDLER_TS)?;
451    std::fs::write(hook_dir.join("HOOK.md"), OPENCLAW_HOOK_MD)?;
452    writeln!(out, "  created  ~/.openclaw/hooks/kaizen-events/handler.ts").unwrap();
453    let _ = std::process::Command::new("openclaw")
454        .args(["hooks", "enable", "kaizen-events"])
455        .status();
456    for event in OPENCLAW_HOOK_EVENTS {
457        let _ = std::process::Command::new("openclaw")
458            .args(["hooks", "subscribe", "kaizen-events", event])
459            .status();
460    }
461    Ok(())
462}
463
464/// Read-only: `~/.openclaw/hooks/kaizen-events` absent / wired / partial.
465pub fn openclaw_kaizen_hook_wiring(_ws: &Path) -> Result<Option<bool>, String> {
466    let Some(hook_dir) = openclaw_hooks_dir() else {
467        return Ok(None);
468    };
469    if !hook_dir.is_dir() {
470        return Ok(None);
471    }
472    let handler_path = hook_dir.join("handler.ts");
473    let hook_md = hook_dir.join("HOOK.md");
474    if !handler_path.exists() || !hook_md.exists() {
475        return Ok(Some(false));
476    }
477    let raw = std::fs::read_to_string(&handler_path).map_err(|e| e.to_string())?;
478    Ok(Some(openclaw_handler_contains_kaizen(&raw)))
479}
480
481fn openclaw_handler_contains_kaizen(raw: &str) -> bool {
482    raw.contains(KAIZEN_OPENCLAW_HOOK_CMD)
483        || (raw.contains(r#"spawn("kaizen""#) && raw.contains(KAIZEN_OPENCLAW_SPAWN_ARGS))
484}
485
486/// Text that `kaizen init` would print to stdout.
487pub fn init_text(workspace: Option<&std::path::Path>) -> Result<String> {
488    init_text_with_options(workspace, InitOptions::default())
489}
490
491pub fn init_text_with_options(
492    workspace: Option<&std::path::Path>,
493    options: InitOptions,
494) -> Result<String> {
495    let ws = match workspace {
496        Some(p) => p.to_path_buf(),
497        None => std::env::current_dir()?,
498    };
499    let mut out = String::new();
500    if let Ok(data_dir) = crate::core::paths::project_data_dir(&ws) {
501        match crate::core::legacy_import::import_legacy(&ws, &data_dir) {
502            Ok(crate::core::legacy_import::ImportOutcome::Imported) => {
503                writeln!(out, "  imported  .kaizen/ → {}", data_dir.display()).unwrap();
504            }
505            Ok(crate::core::legacy_import::ImportOutcome::Conflict) => {
506                writeln!(
507                    out,
508                    "  warning  .kaizen/ and {} both non-empty — skipping legacy import",
509                    data_dir.display()
510                )
511                .unwrap();
512            }
513            _ => {}
514        }
515    }
516    ensure_config(&mut out, &ws)?;
517    patch_cursor_hooks(&mut out, &ws)?;
518    patch_claude_settings(&mut out, &ws)?;
519    patch_openclaw_handlers(&mut out, &ws)?;
520    write_skill(&mut out, &ws)?;
521    write_eval_skill(&mut out, &ws)?;
522    let cws = crate::core::workspace::canonical(&ws);
523    if let Err(e) = crate::core::machine_registry::record_init(&cws) {
524        tracing::warn!("machine registry: {e:#}");
525    }
526    if options.start_capture {
527        append_capture_status(&mut out, &cws, options.deep);
528    }
529    writeln!(out).unwrap();
530    writeln!(
531        out,
532        "kaizen init complete — Cursor + Claude Code + OpenClaw hooks wired."
533    )
534    .unwrap();
535    writeln!(out).unwrap();
536    writeln!(out, "Run Cursor or Claude Code in this repo once, then:").unwrap();
537    writeln!(
538        out,
539        "  kaizen summary            # cost + rollups (agent / model)"
540    )
541    .unwrap();
542    writeln!(
543        out,
544        "  kaizen insights           # activity, top tools, guidance"
545    )
546    .unwrap();
547    writeln!(out, "  kaizen tui                # live session browser").unwrap();
548    writeln!(out, "  kaizen retro --days 7     # weekly heuristic bets").unwrap();
549    writeln!(out).unwrap();
550    writeln!(
551        out,
552        "Agents: `kaizen mcp` exposes every command as MCP tools — see docs/mcp.md."
553    )
554    .unwrap();
555    if let Ok(data_dir) = crate::core::paths::project_data_dir(&ws) {
556        writeln!(out).unwrap();
557        writeln!(out, "Project data: {}", data_dir.display()).unwrap();
558    }
559    Ok(out)
560}
561
562fn append_capture_status(out: &mut String, ws: &Path, deep: bool) {
563    if !crate::daemon::enabled() {
564        writeln!(out, "  skipped  daemon capture (KAIZEN_DAEMON=0)").unwrap();
565        return;
566    }
567    let workspace = ws.to_string_lossy().to_string();
568    match crate::daemon::ensure_capture_blocking(workspace, deep) {
569        Ok(status) => write_capture_status(out, &status),
570        Err(err) => writeln!(out, "  warning  daemon capture unavailable: {err:#}").unwrap(),
571    }
572}
573
574fn write_capture_status(out: &mut String, status: &CaptureStatus) {
575    writeln!(out, "  ready    daemon capture").unwrap();
576    writeln!(
577        out,
578        "  ready    {}",
579        status_line("watchers", &status.watchers)
580    )
581    .unwrap();
582    writeln!(out, "  ready    {}", status_line("hooks", &status.hooks)).unwrap();
583    if status.deep {
584        writeln!(out, "  partial  deep capture ({})", status.proxies.len()).unwrap();
585    }
586    for err in &status.errors {
587        writeln!(out, "  warning  {err}").unwrap();
588    }
589}
590
591fn status_line(label: &str, components: &[crate::ipc::CaptureComponent]) -> String {
592    let ready = components
593        .iter()
594        .filter(|c| c.status == CaptureComponentStatus::Ready)
595        .count();
596    format!("{label}: {ready}/{}", components.len())
597}
598
599/// Idempotent workspace setup.
600pub fn cmd_init(workspace: Option<&Path>, deep: bool) -> Result<()> {
601    print!(
602        "{}",
603        init_text_with_options(
604            workspace,
605            InitOptions {
606                deep,
607                start_capture: true,
608            },
609        )?
610    );
611    Ok(())
612}