on master help sign in
Create an editable default config when none exists

Instead of erroring with "no config", j now materialises the default config at the user config path, owned by and editable by the current user (0600 on unix), then loads it. The default is embedded in the binary, so j works even when the share/j/config.j file is not installed next to it.

commite8d9fc22c2b2d96a0e8a366b5d70a7ae9e35af80 authoriko <mail@iko.soy> dateWed 16 Sep 2026, 18:35 (3 weeks ago) parentc1daaf7
2 files changed+42−22
src/main.rs+27−18 tests/cli.rs+15−4
src/main.rs+27−18
@@ -38,19 +38,25 @@ fn config_path() -> String {
3838 format!("{}/.config/j/config.j", home)
3939}
4040
41+/// The default config, embedded so `j` always has it regardless of how it was
42+/// installed (the share/j/config.j file may not exist next to the binary).
43+const DEFAULT_CONFIG: &str = include_str!("../config.j");
44+
4145fn load_config_file() -> Result<(Config, String), ExitCode> {
4246 let path = config_path();
4347 let src = match std::fs::read_to_string(&path) {
4448 Ok(s) => s,
4549 Err(_) => {
46− return Err(err(
47− 3,
48− format!(
49− "no config at {}; the reference config.j is installed at {}",
50− path,
51− reference_config_path()
52− ),
53− ));
50+ // No user config yet: materialise the default there, editable by
51+ // the current user, so they have a starting point to customise.
52+ if let Err(e) = write_default_config(&path) {
53+ return Err(err(3, format!("cannot create config at {}: {}", path, e)));
54+ }
55+ eprintln!("j: created an editable default config at {}", path);
56+ match std::fs::read_to_string(&path) {
57+ Ok(s) => s,
58+ Err(e) => return Err(err(3, format!("cannot read config at {}: {}", path, e))),
59+ }
5460 }
5561 };
5662 match config::load_config(&src) {
@@ -59,17 +65,20 @@ fn load_config_file() -> Result<(Config, String), ExitCode> {
5965 }
6066}
6167
62−fn reference_config_path() -> String {
63− // installed next to the binary under <prefix>/share/j/config.j
64− if let Ok(exe) = std::env::current_exe() {
65− if let Some(p) = exe.parent().and_then(|p| p.parent()) {
66− let cand = p.join("share/j/config.j");
67− if cand.exists() {
68− return cand.display().to_string();
69− }
70− }
68+/// Write the embedded default config to `path`, creating parent directories
69+/// and making the file readable and writable by the current user.
70+fn write_default_config(path: &str) -> std::io::Result<()> {
71+ if let Some(parent) = std::path::Path::new(path).parent() {
72+ std::fs::create_dir_all(parent)?;
73+ }
74+ std::fs::write(path, DEFAULT_CONFIG)?;
75+ // user-read+write (0o600 on unix): the file is meant to be edited by them
76+ #[cfg(unix)]
77+ {
78+ use std::os::unix::fs::PermissionsExt;
79+ std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
7180 }
72− "/usr/local/share/j/config.j".into()
81+ Ok(())
7382}
7483
7584const RESERVED: &[&str] = &["init", "clone", "remote", "fetch", "push", "undo", "redo", "ops"];
tests/cli.rs+15−4
@@ -450,7 +450,7 @@ fn init_refuses_inside_existing_repo() {
450450}
451451
452452#[test]
453−fn missing_config_is_exit_3() {
453+fn missing_config_is_created_editable() {
454454 let dir = std::env::temp_dir().join(format!(
455455 "j-cli-nocfg-{}-{}",
456456 std::process::id(),
@@ -461,14 +461,25 @@ fn missing_config_is_exit_3() {
461461 ));
462462 let _ = std::fs::remove_dir_all(&dir);
463463 std::fs::create_dir_all(&dir).unwrap();
464+ let cfg_home = dir.join("nonexistent");
464465 let out = Command::new(j_bin())
465466 .arg("status")
466467 .current_dir(&dir)
467− .env("XDG_CONFIG_HOME", dir.join("nonexistent"))
468+ .env("XDG_CONFIG_HOME", &cfg_home)
468469 .output()
469470 .unwrap();
470− assert_eq!(out.status.code(), Some(3));
471+ // no config error: the default config is materialised instead
471472 let stderr = String::from_utf8_lossy(&out.stderr);
472− assert!(stderr.contains("no config"), "{}", stderr);
473+ assert!(!stderr.contains("no config"), "{}", stderr);
474+ assert!(stderr.contains("created an editable default config"), "{}", stderr);
475+ // the config now exists at the user path and is editable by the owner
476+ let cfg = cfg_home.join("j/config.j");
477+ let written = std::fs::read_to_string(&cfg).expect("config was created");
478+ assert!(written.contains("treeWith"), "default config content");
479+ #[cfg(unix)]
480+ {
481+ use std::os::unix::fs::PermissionsExt;
482+ assert_eq!(std::fs::metadata(&cfg).unwrap().permissions().mode() & 0o777, 0o600);
483+ }
473484 let _ = std::fs::remove_dir_all(&dir);
474485}