diff --git a/CHANGELOG.md b/CHANGELOG.md index 232bab8..92daf04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Space module with trait-based abstraction pattern for easy testing and extensibility +- Filesystem module with local filesystem implementation - Initial project structure with workspace layout - CLI foundation with clap argument parsing - TUI crate placeholder (feature-gated) diff --git a/Cargo.lock b/Cargo.lock index 6858635..f6bb64a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,37 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -67,6 +98,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "append-only-bytes" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac436601d6bdde674a0d7fb593e829ffe7b3387c351b356dd20e2d40f5bf3ee5" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ccd462b64c3c72f1be8305905a85d85403d768e8690c9b8bd3b9009a5761679" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "backtrace" version = "0.3.76" @@ -97,18 +170,71 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.54" @@ -137,10 +263,10 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -149,6 +275,15 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -156,289 +291,1398 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "errno" -version = "0.3.14" +name = "console" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" dependencies = [ + "encode_unicode", "libc", + "once_cell", + "unicode-width 0.2.2", "windows-sys 0.61.2", ] [[package]] -name = "flow" -version = "0.1.0" +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "clap", - "flow-cli", - "flow-gui", - "flow-server", - "flow-tui", - "miette", - "thiserror", - "tokio", + "unicode-segmentation", ] [[package]] -name = "flow-app" -version = "0.1.0" +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "miette", - "thiserror", - "tokio", + "libc", ] [[package]] -name = "flow-cli" -version = "0.1.0" +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "cross-xdg" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd89fbf9e0d3bf9c91e17c09fd875857f9d71c3cbe29ae2ab253edd080b17ce5" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "clap", - "miette", - "thiserror", - "tokio", + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", ] [[package]] -name = "flow-core" -version = "0.1.0" +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "miette", - "thiserror", - "tokio", + "winapi", ] [[package]] -name = "flow-gui" -version = "0.1.0" +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "clap", - "miette", - "thiserror", - "tokio", + "generic-array", + "typenum", ] [[package]] -name = "flow-server" -version = "0.1.0" +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "clap", - "miette", - "thiserror", - "tokio", + "darling_core", + "darling_macro", ] [[package]] -name = "flow-tui" -version = "0.1.0" +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "clap", - "miette", - "thiserror", - "tokio", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.111", ] [[package]] -name = "gimli" -version = "0.32.3" +name = "darling_macro" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] [[package]] -name = "heck" -version = "0.5.0" +name = "derive_arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] -name = "is_ci" -version = "1.2.0" +name = "derive_more" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "derive_more-impl" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", +] [[package]] -name = "libc" -version = "0.2.178" +name = "diff" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] -name = "linux-raw-sys" -version = "0.11.0" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "scopeguard", + "litrs", ] [[package]] -name = "memchr" -version = "2.7.6" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "miette" -version = "7.6.0" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "ensure-cov" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33753185802e107b8fa907192af1f0eca13b1fb33327a59266d650fef29b2b4e" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "miette-derive" -version = "7.6.0" +name = "enum-as-inner" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "enum_dispatch" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" dependencies = [ - "adler2", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "mio" -version = "1.1.1" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "wasi", "windows-sys 0.61.2", ] [[package]] -name = "object" -version = "0.37.3" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "find-msvc-tools" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] -name = "owo-colors" -version = "4.2.3" +name = "flow" +version = "0.1.0" +dependencies = [ + "clap", + "flow-cli", + "flow-gui", + "flow-server", + "flow-tui", + "miette", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-app" +version = "0.1.0" +dependencies = [ + "miette", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-cli" +version = "0.1.0" +dependencies = [ + "clap", + "console", + "flow-core", + "inquire", + "miette", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-core" +version = "0.1.0" +dependencies = [ + "cross-xdg", + "loro", + "miette", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-gui" +version = "0.1.0" +dependencies = [ + "clap", + "miette", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-server" +version = "0.1.0" +dependencies = [ + "clap", + "miette", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "flow-tui" +version = "0.1.0" +dependencies = [ + "clap", + "miette", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-btree" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c1bce85c110ab718fd139e0cc89c51b63bd647b14a767e24bdfc77c83df79b" +dependencies = [ + "arref", + "heapless 0.9.2", + "itertools 0.11.0", + "loro-thunderdome", + "proc-macro2", + "rustc-hash", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "inquire" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae51d5da01ce7039024fbdec477767c102c454dbdb09d4e2a432ece705b1b25d" +dependencies = [ + "bitflags", + "chrono", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "tempfile", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "loro" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75216d8f99725531a30f7b00901ee154a4f8a9b7f125bfe032e197d4c7ffb8c" +dependencies = [ + "enum-as-inner 0.6.1", + "generic-btree", + "loro-common", + "loro-delta", + "loro-internal", + "loro-kv-store", + "rustc-hash", + "tracing", +] + +[[package]] +name = "loro-common" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70363ea05a9c507fd9d58b65dc414bf515f636d69d8ab53e50ecbe8d27eef90c" +dependencies = [ + "arbitrary", + "enum-as-inner 0.6.1", + "leb128", + "loro-rle", + "nonmax", + "rustc-hash", + "serde", + "serde_columnar", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "loro-delta" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eafa788a72c1cbf0b7dc08a862cd7cc31b96d99c2ef749cdc94c2330f9494d3" +dependencies = [ + "arrayvec", + "enum-as-inner 0.5.1", + "generic-btree", + "heapless 0.8.0", +] + +[[package]] +name = "loro-internal" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f447044ec3d3ba572623859add3334bd87b84340ee5fdf00315bfee0e3ad3e3f" +dependencies = [ + "append-only-bytes", + "arref", + "bytes", + "either", + "ensure-cov", + "enum-as-inner 0.6.1", + "enum_dispatch", + "generic-btree", + "getrandom 0.2.17", + "im", + "itertools 0.12.1", + "leb128", + "loom", + "loro-common", + "loro-delta", + "loro-kv-store", + "loro-rle", + "loro_fractional_index", + "md5", + "nonmax", + "num", + "num-traits", + "once_cell", + "parking_lot", + "pest", + "pest_derive", + "postcard", + "pretty_assertions", + "rand", + "rustc-hash", + "serde", + "serde_columnar", + "serde_json", + "smallvec", + "thiserror 1.0.69", + "thread_local", + "tracing", + "wasm-bindgen", + "xxhash-rust", +] + +[[package]] +name = "loro-kv-store" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78beebc933a33c26495c9a98f05b38bc0a4c0a337ecfbd3146ce1f9437eec71f" +dependencies = [ + "bytes", + "ensure-cov", + "loro-common", + "lz4_flex", + "once_cell", + "quick_cache", + "rustc-hash", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "loro-rle" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76400c3eea6bb39b013406acce964a8db39311534e308286c8d8721baba8ee20" +dependencies = [ + "append-only-bytes", + "num", + "smallvec", +] + +[[package]] +name = "loro-thunderdome" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" + +[[package]] +name = "loro_fractional_index" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c8ea186958094052b971fe7e322a934b034c3bf62f0458ccea04fcd687ba1" +dependencies = [ + "once_cell", + "rand", + "serde", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pest" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", + "serde", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick_cache" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3" +dependencies = [ + "ahash", + "equivalent", + "hashbrown", + "parking_lot", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_columnar" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "2a16e404f17b16d0273460350e29b02d76ba0d70f34afdc9a4fa034c97d6c6eb" +dependencies = [ + "itertools 0.11.0", + "postcard", + "serde", + "serde_columnar_derive", + "thiserror 1.0.69", +] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "serde_columnar_derive" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "45958fce4903f67e871fbf15ac78e289269b21ebd357d6fecacdba233629112e" dependencies = [ - "lock_api", - "parking_lot_core", + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "serde_derive", ] [[package]] -name = "pin-project-lite" -version = "0.2.16" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "unicode-ident", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] -name = "quote" -version = "1.0.42" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "proc-macro2", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "bitflags", + "lazy_static", ] [[package]] -name = "rustc-demangle" -version = "0.1.27" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "rustix" -version = "1.1.3" +name = "signal-hook" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ - "bitflags", - "errno", "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "signal-hook-registry", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "signal-hook-mio" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] [[package]] name = "signal-hook-registry" @@ -450,11 +1694,24 @@ dependencies = [ "libc", ] +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -466,6 +1723,21 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -493,6 +1765,17 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.111" @@ -504,6 +1787,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -524,13 +1820,33 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -541,7 +1857,16 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] @@ -569,9 +1894,88 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -584,6 +1988,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" @@ -602,18 +2012,159 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -696,3 +2247,47 @@ name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/crates/flow-cli/Cargo.toml b/crates/flow-cli/Cargo.toml index 5ab8648..33e0551 100644 --- a/crates/flow-cli/Cargo.toml +++ b/crates/flow-cli/Cargo.toml @@ -13,10 +13,15 @@ categories.workspace = true rust-version.workspace = true [dependencies] +flow-core = { workspace = true } clap = { workspace = true } tokio = { workspace = true } miette = { workspace = true } thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +inquire = { version = "0.9.2", features = ["date", "editor"] } +console = "0.16.2" [lints] workspace = true diff --git a/crates/flow-cli/src/commands.rs b/crates/flow-cli/src/commands.rs new file mode 100644 index 0000000..74228a5 --- /dev/null +++ b/crates/flow-cli/src/commands.rs @@ -0,0 +1,39 @@ +use miette::Result; + +use crate::{common::OutputArgs, printer::Printer}; + +pub mod init; + +pub trait Command: Sized { + type Args; + type Output: serde::Serialize; + + fn new(args: Self::Args) -> Self; + + fn output_args(&self) -> &OutputArgs; + + fn printer(&self) -> Printer { + self.output_args().printer() + } + + async fn interactive(&mut self) -> Result<()>; + + async fn execute(&mut self) -> Result; + + fn finalize(&self, output: &Self::Output); + + async fn run(&mut self) -> Result<()> { + if !self.output_args().json { + self.interactive().await?; + } + + let output = self.execute().await?; + if self.output_args().json { + self.printer().json(&output)?; + } else { + self.finalize(&output); + } + + Ok(()) + } +} diff --git a/crates/flow-cli/src/commands/init.rs b/crates/flow-cli/src/commands/init.rs new file mode 100644 index 0000000..e966ad3 --- /dev/null +++ b/crates/flow-cli/src/commands/init.rs @@ -0,0 +1,120 @@ +use std::path::PathBuf; + +use clap::Args; +use flow_core::{Config, Space}; +use inquire::Text; +use miette::IntoDiagnostic; +use serde::Serialize; + +use crate::{commands::Command, common::OutputArgs, errors::Error, extensions::PathExt}; + +#[derive(Args, Debug, Clone)] +pub struct Arguments { + #[command(flatten)] + pub output: OutputArgs, + + /// Path to initialize the space at + pub path: Option, + + /// Name of the space (defaults to the directory name) + #[arg(short, long)] + pub name: Option, + + /// Flag, to not register the space + #[arg(long)] + pub no_register: bool, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Output { + pub name: String, + pub path: PathBuf, +} + +pub struct Init { + args: Arguments, +} + +impl Command for Init { + type Args = Arguments; + type Output = Output; + + fn new(args: Self::Args) -> Self { + Self { args } + } + + fn output_args(&self) -> &OutputArgs { + &self.args.output + } + + async fn interactive(&mut self) -> miette::Result<()> { + if self.args.path.is_none() { + self.printer().info("Entering interactive mode"); + + let path_input = Text::new("Path:") + .with_default(".") + .with_help_message("Path where the space will be initialized") + .prompt() + .into_diagnostic()?; + + self.args.path = Some(PathBuf::from(path_input)); + } + + if self.args.name.is_none() { + let mut name_prompt = Text::new("Name:").with_help_message("Name of the space"); + let test = self + .args + .path + .as_ref() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()); + if let Some(name_default) = test { + name_prompt = name_prompt.with_default(name_default); + } + + let name_input = name_prompt.prompt().into_diagnostic()?; + if !name_input.trim().is_empty() { + self.args.name = Some(name_input); + } + } + + Ok(()) + } + + async fn execute(&mut self) -> miette::Result { + let path = self + .args + .path + .take() + .ok_or_else(|| Error::MissingArgument("path".to_string()))?; + + let path_name = path.file_name().and_then(|n| n.to_str()); + let name = self + .args + .name + .take() + .or_else(|| path_name.map(String::from)) + .ok_or_else(|| Error::MissingArgument("name".to_string()))?; + + let space = Space::init(&path, &name).await?; + if !self.args.no_register { + let mut config = Config::load().await?; + + config.register(&space).await?; + if config.active().is_none() { + config.set_active(space.name()).await?; + } + } + + Ok(Output { name, path }) + } + + fn finalize(&self, output: &Self::Output) { + let printer = self.printer(); + + printer.success("Space initialized"); + printer.blank(); + printer.kv("Name", &output.name); + printer.kv("Path", output.path.normalize()); + } +} diff --git a/crates/flow-cli/src/common.rs b/crates/flow-cli/src/common.rs new file mode 100644 index 0000000..b9b19f8 --- /dev/null +++ b/crates/flow-cli/src/common.rs @@ -0,0 +1,81 @@ +use clap::Args; +use flow_core::{Config, Space}; +use miette::Result; +use std::path::PathBuf; + +use crate::{errors::Error, printer::Printer}; + +/// Output formatting arguments - available for ALL commands. +#[derive(Args, Debug, Clone)] +pub struct OutputArgs { + /// Output in JSON format + #[arg(long, global = true)] + pub json: bool, + + /// Detailed logging + #[arg(short, long, global = true)] + pub verbose: bool, + + /// Suppress non-error output + #[arg(short, long, global = true)] + pub quiet: bool, +} + +impl OutputArgs { + /// Create a printer from these output arguments. + #[must_use] + pub const fn printer(&self) -> Printer { + Printer::new(self.json, self.verbose, self.quiet) + } +} + +/// Arguments for commands that operate on existing spaces. +#[derive(Args, Debug, Clone)] +pub struct SpaceArgs { + /// Embedded output arguments + #[command(flatten)] + pub output: OutputArgs, + + /// Target specific space by name or path (overrides active space) + #[arg(long)] + pub space: Option, +} + +impl SpaceArgs { + /// Create a printer from these space arguments. + #[must_use] + #[allow(dead_code)] + pub const fn printer(&self) -> Printer { + self.output.printer() + } + + /// Load the space based on the provided arguments. + /// + /// Resolution order: + /// 1. If `--space` is provided and is a valid path, load from that path + /// 2. If `--space` is provided and matches a registered space name, load that space + /// 3. Otherwise, load the currently active space from config + /// + /// # Errors + /// + /// Returns an error if no space can be found or loaded. + #[allow(dead_code)] + pub async fn load_space(&self) -> Result { + if let Some(name_or_path) = &self.space { + let path = PathBuf::from(name_or_path); + if path.exists() { + return Space::load(path).await; + } + } + + let config = Config::load().await?; + let space = self + .space + .as_ref() + .and_then(|n| config.find(n)) + .or_else(|| config.active()) + .ok_or(Error::NoActiveSpace)?; + + Space::load(&space.name).await + } +} diff --git a/crates/flow-cli/src/errors.rs b/crates/flow-cli/src/errors.rs new file mode 100644 index 0000000..ffde8ea --- /dev/null +++ b/crates/flow-cli/src/errors.rs @@ -0,0 +1,21 @@ +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Error, Diagnostic)] +pub enum Error { + #[error("Filesystem error: {0}")] + #[diagnostic(code(flow::io_error), url(docsrs))] + Io(#[from] std::io::Error), + + #[error("Missing argument: {0}")] + #[diagnostic(code(flow::missing_argument), url(docsrs))] + MissingArgument(String), + + #[error("There is no active space set")] + #[diagnostic( + code(flow::missing_argument), + url(docsrs), + help(". Use --space to specify one specifically or register one with `flow register`.") + )] + NoActiveSpace, +} diff --git a/crates/flow-cli/src/extensions.rs b/crates/flow-cli/src/extensions.rs new file mode 100644 index 0000000..5848f20 --- /dev/null +++ b/crates/flow-cli/src/extensions.rs @@ -0,0 +1,21 @@ +use std::path::Path; + +pub trait PathExt { + fn normalize(&self) -> String; +} + +impl PathExt for Path { + fn normalize(&self) -> String { + let s = self.display().to_string(); + + #[cfg(windows)] + { + // Remove Windows extended-length path prefix + if let Some(stripped) = s.strip_prefix(r"\\?\") { + return stripped.to_string(); + } + } + + s + } +} diff --git a/crates/flow-cli/src/lib.rs b/crates/flow-cli/src/lib.rs index 8f37ba0..c4538db 100644 --- a/crates/flow-cli/src/lib.rs +++ b/crates/flow-cli/src/lib.rs @@ -1,9 +1,17 @@ use clap::Subcommand; use miette::Result; +use crate::commands::{init, Command}; + +mod commands; +mod common; +mod errors; +mod extensions; +mod printer; + #[derive(Subcommand)] pub enum Commands { - Test, + Init(init::Arguments), } /// Run the CLI with the given command. @@ -11,9 +19,9 @@ pub enum Commands { /// # Errors /// /// Returns an error if the command execution fails. -pub fn run(cmd: &Commands) -> Result<()> { +pub async fn run(cmd: Commands) -> Result<()> { match cmd { - Commands::Test => println!("This is a test"), + Commands::Init(args) => init::Init::new(args).run().await?, } Ok(()) diff --git a/crates/flow-cli/src/printer.rs b/crates/flow-cli/src/printer.rs new file mode 100644 index 0000000..18b0c52 --- /dev/null +++ b/crates/flow-cli/src/printer.rs @@ -0,0 +1,114 @@ +use console::{style, Emoji, Term}; +use miette::{IntoDiagnostic, Result}; + +// Emojis with fallbacks for terminals that don't support them +static SPARKLE: Emoji<'_, '_> = Emoji("✨ ", "* "); +static INFO: Emoji<'_, '_> = Emoji("ℹ️ ", "[i] "); +static SUCCESS: Emoji<'_, '_> = Emoji("✅ ", "[+] "); +static WARN: Emoji<'_, '_> = Emoji("⚠️ ", "[!] "); +static ERROR: Emoji<'_, '_> = Emoji("❌ ", "[x] "); +static DEBUG: Emoji<'_, '_> = Emoji("🔍 ", "[?] "); +static ARROW: Emoji<'_, '_> = Emoji("→ ", "-> "); + +/// Handles all CLI output printing with support for JSON, verbose, and quiet modes. +#[derive(Debug, Clone)] +pub struct Printer { + json: bool, + verbose: bool, + quiet: bool, +} + +impl Printer { + #[must_use] + pub const fn new(json: bool, verbose: bool, quiet: bool) -> Self { + Self { json, verbose, quiet } + } + + #[must_use] + pub const fn is_json(&self) -> bool { + self.json + } + + pub fn print(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(message.as_ref()); + } + } + + pub fn success(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", SUCCESS, style(message.as_ref()).green().bold())); + } + } + + pub fn info(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", INFO, style(message.as_ref()).cyan())); + } + } + + pub fn warning(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", WARN, style(message.as_ref()).yellow().bold())); + } + } + + pub fn step(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", ARROW, style(message.as_ref()).dim())); + } + } + + pub fn verbose(&self, message: impl AsRef) { + if self.verbose && !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", DEBUG, style(message.as_ref()).dim())); + } + } + + pub fn debug(&self, label: impl Into, value: impl Into) { + if self.verbose && !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!( + "{}{}: {}", + DEBUG, + style(label.into()).dim(), + style(value.into()).dim().italic() + )); + } + } + + pub fn error(&self, message: impl AsRef) { + if !self.quiet && !self.json { + let _ = Term::stderr().write_line(&format!("{}{}", ERROR, style(message.as_ref()).red().bold())); + } + } + + pub fn heading(&self, heading: impl Into) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!("{}{}", SPARKLE, style(heading.into()).bold().underlined())); + } + } + + pub fn kv(&self, key: impl Into, value: impl Into) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(&format!( + " {}: {}", + style(key.into()).cyan().bold(), + style(value.into()).white() + )); + } + } + + pub fn blank(&self) { + if !self.quiet && !self.json { + let _ = Term::stdout().write_line(""); + } + } + + pub fn json(&self, value: &T) -> Result<()> { + if self.json { + let json = serde_json::to_string_pretty(value).into_diagnostic()?; + let _ = Term::stdout().write_line(&json); + } + Ok(()) + } +} diff --git a/crates/flow-core/Cargo.toml b/crates/flow-core/Cargo.toml index 3c4b000..ce44a37 100644 --- a/crates/flow-core/Cargo.toml +++ b/crates/flow-core/Cargo.toml @@ -16,6 +16,10 @@ rust-version.workspace = true tokio = { workspace = true } miette = { workspace = true } thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +loro = "1.10.3" +cross-xdg = "2.1" [lints] workspace = true diff --git a/crates/flow-core/src/config.rs b/crates/flow-core/src/config.rs new file mode 100644 index 0000000..5d07f3a --- /dev/null +++ b/crates/flow-core/src/config.rs @@ -0,0 +1,366 @@ +//! Configuration management for Flow. +//! +//! This module provides the configuration system for Flow, managing both +//! user settings and the registry of known spaces. +//! +//! # Overview +//! +//! Flow stores its configuration in `~/.config/flow/` with two files: +//! +//! - `config.json` - User preferences and settings +//! - `spaces.json` - Registered spaces and the active space +//! +//! The [`Config`] struct provides the main interface for working with +//! configuration. It supports: +//! +//! - Registering and unregistering spaces +//! - Setting and clearing the active space +//! - Looking up spaces by name or path +//! - Accessing user settings +//! +//! # Examples +//! +//! ## Loading configuration +//! +//! ```no_run +//! use flow_core::Config; +//! +//! # async fn example() -> miette::Result<()> { +//! let config = Config::load().await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Registering a space +//! +//! ```no_run +//! use std::path::Path; +//! use flow_core::{Config, Space}; +//! +//! # async fn example() -> miette::Result<()> { +//! let space = Space::init(Path::new("./notes"), "personal").await?; +//! let mut config = Config::load().await?; +//! config.register(&space).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Setting the active space +//! +//! ```no_run +//! use flow_core::Config; +//! +//! # async fn example() -> miette::Result<()> { +//! let mut config = Config::load().await?; +//! config.set_active("personal").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Architecture +//! +//! The public [`Config`] struct is a thin wrapper around an internal +//! implementation (`DefaultConfig`). This design allows us to: +//! +//! - Keep the public API simple and stable +//! - Inject different filesystem implementations for testing +//! - Potentially support different configuration backends in the future + +use miette::Result; + +use crate::filesystem::LocalFilesystem; +use crate::space::{Locator, Space}; + +use self::default::DefaultConfig; +use self::traits::Config as _; + +mod default; +mod traits; +mod types; + +pub use self::types::{RegisteredSpace, Settings}; + +/// Flow configuration manager. +/// +/// `Config` is the main entry point for managing Flow's configuration. +/// It provides methods to register spaces, set the active space, and +/// access user settings. +/// +/// # Examples +/// +/// ```no_run +/// use std::path::Path; +/// use flow_core::{Config, Space}; +/// +/// # async fn example() -> miette::Result<()> { +/// // Load configuration (creates default if none exists) +/// let mut config = Config::load().await?; +/// +/// // Initialize and register a space +/// let space = Space::init(Path::new("./work-notes"), "work").await?; +/// config.register(&space).await?; +/// +/// // Set it as active +/// config.set_active("work").await?; +/// +/// // Look up a space +/// if let Some(registered) = config.find("work") { +/// println!("Found space at: {}", registered.path.display()); +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// # Thread Safety +/// +/// `Config` is `Send` and `Sync`, making it safe to share across threads +/// and use in async contexts. +pub struct Config { + /// The underlying configuration implementation. + inner: DefaultConfig, +} + +impl Config { + /// Loads the configuration from disk. + /// + /// If the configuration files don't exist, they will be created with + /// default values in `~/.config/flow/`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The configuration directory cannot be created + /// - The configuration files cannot be read or written + /// - The configuration files contain invalid JSON + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let config = Config::load().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn load() -> Result { + let fs = LocalFilesystem; + let inner = DefaultConfig::load(fs).await?; + Ok(Self { inner }) + } + + /// Registers a space in the configuration. + /// + /// The space will be added to the list of known spaces, allowing it + /// to be opened by name in the future. + /// + /// # Arguments + /// + /// * `space` - A reference to the space to register. + /// + /// # Errors + /// + /// Returns an error if: + /// - A space with the same name is already registered + /// - A space at the same path is already registered + /// - The configuration cannot be saved + /// + /// # Examples + /// + /// ```no_run + /// use std::path::Path; + /// use flow_core::{Config, Space}; + /// + /// # async fn example() -> miette::Result<()> { + /// let space = Space::init(Path::new("./notes"), "personal").await?; + /// let mut config = Config::load().await?; + /// config.register(&space).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn register(&mut self, space: &Space) -> Result<()> { + self.inner.register(space).await + } + + /// Unregisters a space from the configuration. + /// + /// The space will be removed from the list of known spaces. If the + /// space was the active space, the active space will be cleared. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to unregister, either by name or path. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space is not registered + /// - The configuration cannot be saved + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let mut config = Config::load().await?; + /// config.unregister("personal").await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn unregister(&mut self, locator: impl Into + Send) -> Result<()> { + self.inner.unregister(locator).await + } + + /// Sets the active space. + /// + /// The active space is the default space used when no space is + /// explicitly specified in commands. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to set as active, either by name or path. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space is not registered + /// - The configuration cannot be saved + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let mut config = Config::load().await?; + /// config.set_active("personal").await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn set_active(&mut self, locator: impl Into + Send) -> Result<()> { + self.inner.set_active(locator).await + } + + /// Clears the active space. + /// + /// After calling this, no space will be active until one is explicitly + /// set again. + /// + /// # Errors + /// + /// Returns an error if the configuration cannot be saved. + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let mut config = Config::load().await?; + /// config.clear_active().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn clear_active(&mut self) -> Result<()> { + self.inner.clear_active().await + } + + /// Returns the currently active space, if any. + /// + /// # Returns + /// + /// A reference to the active space, or `None` if no space is active. + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let config = Config::load().await?; + /// if let Some(space) = config.active() { + /// println!("Active space: {}", space.name); + /// } + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn active(&self) -> Option<&RegisteredSpace> { + self.inner.active() + } + + /// Finds a registered space by name or path. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to find, either by name or path. + /// + /// # Returns + /// + /// A reference to the registered space, or `None` if not found. + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let config = Config::load().await?; + /// if let Some(space) = config.find("personal") { + /// println!("Found at: {}", space.path.display()); + /// } + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn find(&self, locator: impl Into) -> Option<&RegisteredSpace> { + self.inner.find(locator) + } + + /// Returns all registered spaces. + /// + /// # Returns + /// + /// A slice containing all registered spaces. + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let config = Config::load().await?; + /// for space in config.spaces() { + /// println!("{}: {}", space.name, space.path.display()); + /// } + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn spaces(&self) -> &[RegisteredSpace] { + self.inner.spaces() + } + + /// Returns the current user settings. + /// + /// # Returns + /// + /// A reference to the settings. + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Config; + /// + /// # async fn example() -> miette::Result<()> { + /// let config = Config::load().await?; + /// println!("Config version: {}", config.settings().version); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn settings(&self) -> &Settings { + self.inner.settings() + } +} diff --git a/crates/flow-core/src/config/default.rs b/crates/flow-core/src/config/default.rs new file mode 100644 index 0000000..ff859dd --- /dev/null +++ b/crates/flow-core/src/config/default.rs @@ -0,0 +1,228 @@ +//! Default implementation of the [`Config`] trait. +//! +//! This module provides [`DefaultConfig`], the standard implementation of +//! the config trait used in production. It is generic over the filesystem +//! implementation to allow for dependency injection during testing. +//! +//! # Overview +//! +//! The [`DefaultConfig`] struct manages Flow's configuration, including: +//! +//! - User settings stored in `~/.config/flow/config.json` +//! - Registered spaces stored in `~/.config/flow/spaces.json` +//! +//! # Directory Structure +//! +//! Configuration is stored in the XDG config directory: +//! +//! ```text +//! ~/.config/flow/ +//! ├── config.json # User settings +//! └── spaces.json # Registered spaces +//! ``` + +use std::path::PathBuf; + +use cross_xdg::BaseDirs; +use miette::{IntoDiagnostic, Result}; + +use crate::errors::Error; +use crate::filesystem::Filesystem; +use crate::space::Locator; + +use super::traits::Config; +use super::types::{RegisteredSpace, Settings, Spaces}; +use crate::space::Space; + +/// The directory name for Flow configuration under the XDG config home. +const CONFIG_DIR: &str = "flow"; + +/// The filename for user settings. +const SETTINGS_FILE: &str = "config.json"; + +/// The filename for registered spaces. +const SPACES_FILE: &str = "spaces.json"; + +/// The default implementation of Flow configuration. +/// +/// `DefaultConfig` is generic over the filesystem implementation, allowing +/// different storage backends to be used. In production, this is typically +/// [`LocalFilesystem`](crate::filesystem::LocalFilesystem), while tests +/// can inject mock implementations. +/// +/// # Type Parameters +/// +/// * `F` - The filesystem implementation to use for all I/O operations. +/// Must implement [`Filesystem`] and be thread-safe (`Send + Sync`). +pub struct DefaultConfig { + /// The filesystem backend for all I/O operations. + fs: F, + + /// The path to the configuration directory. + config_dir: PathBuf, + + /// User settings. + settings: Settings, + + /// Registered spaces. + spaces: Spaces, +} + +impl DefaultConfig { + /// Returns the path to the configuration directory. + /// + /// Uses `cross-xdg` to determine the XDG config home, then appends + /// the Flow-specific directory name. + fn config_dir() -> Result { + let base_dirs = BaseDirs::new().into_diagnostic()?; + Ok(base_dirs.config_home().join(CONFIG_DIR)) + } + + /// Persists the settings to disk. + #[allow(dead_code)] + async fn save_settings(&self) -> Result<()> { + let path = self.config_dir.join(SETTINGS_FILE); + let json = serde_json::to_string_pretty(&self.settings).into_diagnostic()?; + self.fs.write(&path, json.as_bytes()).await + } + + /// Persists the spaces to disk. + async fn save_spaces(&self) -> Result<()> { + let path = self.config_dir.join(SPACES_FILE); + let json = serde_json::to_string_pretty(&self.spaces).into_diagnostic()?; + self.fs.write(&path, json.as_bytes()).await + } + + /// Finds a space by locator and returns its index. + fn find_index(&self, locator: &Locator) -> Option { + match locator { + Locator::Name(name) => self.spaces.spaces.iter().position(|s| &s.name == name), + Locator::Path(path) => self.spaces.spaces.iter().position(|s| &s.path == path), + } + } +} + +impl Config for DefaultConfig { + type Fs = F; + + async fn load(fs: Self::Fs) -> Result + where + Self: Sized, + { + let config_dir = Self::config_dir()?; + + if !fs.exists(&config_dir).await? { + fs.create_dir_all(&config_dir).await?; + } + + let settings_path = config_dir.join(SETTINGS_FILE); + let settings = if fs.exists(&settings_path).await? { + let json = fs.read_to_string(&settings_path).await?; + serde_json::from_str(&json).into_diagnostic()? + } else { + let settings = Settings { + version: env!("CARGO_PKG_VERSION").to_string(), + }; + let json = serde_json::to_string_pretty(&settings).into_diagnostic()?; + fs.write(&settings_path, json.as_bytes()).await?; + settings + }; + + let spaces_path = config_dir.join(SPACES_FILE); + let spaces = if fs.exists(&spaces_path).await? { + let json = fs.read_to_string(&spaces_path).await?; + serde_json::from_str(&json).into_diagnostic()? + } else { + let spaces = Spaces::default(); + let json = serde_json::to_string_pretty(&spaces).into_diagnostic()?; + fs.write(&spaces_path, json.as_bytes()).await?; + spaces + }; + + Ok(Self { + fs, + config_dir, + settings, + spaces, + }) + } + + async fn register(&mut self, space: &Space) -> Result<()> { + let name = space.name(); + let path = space.path(); + + // Check if already registered by name + if self.spaces.spaces.iter().any(|s| s.name == name) { + return Err(Error::SpaceAlreadyRegistered(name.to_string()).into()); + } + + // Check if already registered by path + if self.spaces.spaces.iter().any(|s| s.path == path) { + return Err(Error::SpacePathAlreadyRegistered(path.to_path_buf()).into()); + } + + self.spaces.spaces.push(RegisteredSpace { + name: name.to_string(), + path: path.to_path_buf(), + }); + + self.save_spaces().await + } + + async fn unregister(&mut self, locator: impl Into + Send) -> Result<()> { + let locator = locator.into(); + + let index = self + .find_index(&locator) + .ok_or_else(|| Error::SpaceNotRegistered(locator.clone()))?; + + let removed = self.spaces.spaces.remove(index); + + // Clear active if it was the removed space + if self.spaces.active.as_ref() == Some(&removed.name) { + self.spaces.active = None; + } + + self.save_spaces().await + } + + async fn set_active(&mut self, locator: impl Into + Send) -> Result<()> { + let locator = locator.into(); + + let space = self + .find(locator.clone()) + .ok_or(Error::SpaceNotRegistered(locator))?; + + self.spaces.active = Some(space.name.clone()); + + self.save_spaces().await + } + + async fn clear_active(&mut self) -> Result<()> { + self.spaces.active = None; + self.save_spaces().await + } + + fn active(&self) -> Option<&RegisteredSpace> { + self.spaces + .active + .as_ref() + .and_then(|name| self.spaces.spaces.iter().find(|s| &s.name == name)) + } + + fn find(&self, locator: impl Into) -> Option<&RegisteredSpace> { + let locator = locator.into(); + match locator { + Locator::Name(name) => self.spaces.spaces.iter().find(|s| s.name == name), + Locator::Path(path) => self.spaces.spaces.iter().find(|s| s.path == path), + } + } + + fn spaces(&self) -> &[RegisteredSpace] { + &self.spaces.spaces + } + + fn settings(&self) -> &Settings { + &self.settings + } +} diff --git a/crates/flow-core/src/config/traits.rs b/crates/flow-core/src/config/traits.rs new file mode 100644 index 0000000..efd57e8 --- /dev/null +++ b/crates/flow-core/src/config/traits.rs @@ -0,0 +1,149 @@ +//! Configuration trait definition. +//! +//! This module defines the [`Config`] trait, which provides the interface +//! for managing Flow's configuration, including registered spaces and +//! user settings. + +use miette::Result; + +use crate::filesystem::Filesystem; +use crate::space::Locator; + +use super::types::{RegisteredSpace, Settings}; +use crate::space::Space; + +/// Configuration management for Flow. +/// +/// This trait defines the interface for loading, saving, and managing +/// Flow's configuration. It handles both user settings and the registry +/// of known spaces. +/// +/// # Implementors +/// +/// - [`DefaultConfig`](super::default::DefaultConfig) - The standard implementation +/// using the filesystem abstraction. +/// +/// # Examples +/// +/// ```ignore +/// use flow_core::config::{Config, DefaultConfig}; +/// use flow_core::filesystem::LocalFilesystem; +/// use flow_core::Space; +/// +/// let fs = LocalFilesystem; +/// let mut config = DefaultConfig::load(fs).await?; +/// +/// // Initialize and register a space +/// let space = Space::init("./notes", "personal").await?; +/// config.register(&space).await?; +/// +/// // Set it as active +/// config.set_active("personal").await?; +/// ``` +pub trait Config: Sized + Send + Sync { + /// The filesystem implementation used for persistence. + type Fs: Filesystem; + + /// Loads the configuration from disk. + /// + /// If the configuration files don't exist, they will be created with + /// default values. + /// + /// # Errors + /// + /// Returns an error if: + /// - The configuration directory cannot be created + /// - The configuration files cannot be read or written + /// - The configuration files contain invalid JSON + async fn load(fs: Self::Fs) -> Result; + + /// Registers a space in the configuration. + /// + /// The space will be added to the list of known spaces, allowing it + /// to be opened by name in the future. + /// + /// # Arguments + /// + /// * `space` - A reference to the space to register. + /// + /// # Errors + /// + /// Returns an error if: + /// - A space with the same name is already registered + /// - The configuration cannot be saved + async fn register(&mut self, space: &Space) -> Result<()>; + + /// Unregisters a space from the configuration. + /// + /// The space will be removed from the list of known spaces. If the + /// space was the active space, the active space will be cleared. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to unregister, either by name or path. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space is not registered + /// - The configuration cannot be saved + async fn unregister(&mut self, locator: impl Into + Send) -> Result<()>; + + /// Sets the active space. + /// + /// The active space is the default space used when no space is + /// explicitly specified in commands. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to set as active, either by name or path. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space is not registered + /// - The configuration cannot be saved + async fn set_active(&mut self, locator: impl Into + Send) -> Result<()>; + + /// Clears the active space. + /// + /// After calling this, no space will be active until one is explicitly + /// set again. + /// + /// # Errors + /// + /// Returns an error if the configuration cannot be saved. + async fn clear_active(&mut self) -> Result<()>; + + /// Returns the currently active space, if any. + /// + /// # Returns + /// + /// A reference to the active space, or `None` if no space is active. + fn active(&self) -> Option<&RegisteredSpace>; + + /// Finds a registered space by name or path. + /// + /// # Arguments + /// + /// * `locator` - Identifies the space to find, either by name or path. + /// + /// # Returns + /// + /// A reference to the registered space, or `None` if not found. + fn find(&self, locator: impl Into) -> Option<&RegisteredSpace>; + + /// Returns all registered spaces. + /// + /// # Returns + /// + /// A slice containing all registered spaces. + fn spaces(&self) -> &[RegisteredSpace]; + + /// Returns the current user settings. + /// + /// # Returns + /// + /// A reference to the settings. + fn settings(&self) -> &Settings; +} diff --git a/crates/flow-core/src/config/types.rs b/crates/flow-core/src/config/types.rs new file mode 100644 index 0000000..75d34a4 --- /dev/null +++ b/crates/flow-core/src/config/types.rs @@ -0,0 +1,112 @@ +//! Configuration data types. +//! +//! This module provides the data structures for Flow's configuration system, +//! including user settings and registered spaces. +//! +//! # Overview +//! +//! Flow stores its configuration in `~/.config/flow/` with two files: +//! +//! - `config.json` - User preferences and settings +//! - `spaces.json` - Registered spaces and the active space +//! +//! # Serialization +//! +//! All types are serialized to JSON using [`serde`]. The format is designed +//! to be human-readable and forward-compatible. + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// User configuration settings. +/// +/// This struct holds user preferences that affect Flow's behavior. +/// It is stored in `~/.config/flow/config.json`. +/// +/// # Serialization Format +/// +/// ```json +/// { +/// "version": "0.1.0" +/// } +/// ``` +/// +/// # Future Extensions +/// +/// This struct is intentionally minimal. Future versions may add fields for: +/// - Editor preferences +/// - Default space settings +/// - UI customization +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Settings { + /// The Flow version that created this configuration. + /// + /// Used for future migrations if the configuration format changes. + pub version: String, +} + +/// A registered space entry. +/// +/// Each registered space has a name and a filesystem path. The name +/// is used for quick access via commands like `flow open `. +/// +/// # Serialization Format +/// +/// ```json +/// { +/// "name": "personal", +/// "path": "/home/user/spaces/personal" +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegisteredSpace { + /// The human-readable name of the space. + /// + /// This name is unique within the registry and is used to + /// identify the space in commands. + pub name: String, + + /// The filesystem path to the space directory. + /// + /// This is an absolute path to the directory containing the + /// `.flow/` subdirectory. + pub path: PathBuf, +} + +/// Registry of known spaces and the active space. +/// +/// This struct maintains the list of all registered spaces and tracks +/// which space is currently active. It is stored in `~/.config/flow/spaces.json`. +/// +/// # Serialization Format +/// +/// ```json +/// { +/// "active": "personal", +/// "spaces": [ +/// { +/// "name": "personal", +/// "path": "/home/user/spaces/personal" +/// }, +/// { +/// "name": "work", +/// "path": "/home/user/spaces/work" +/// } +/// ] +/// } +/// ``` +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Spaces { + /// The name of the currently active space. + /// + /// This is `None` if no space has been activated yet. + /// When set, it must match the name of a space in the `spaces` list. + pub active: Option, + + /// The list of registered spaces. + /// + /// Each space has a unique name that can be used to quickly + /// open or reference it. + pub spaces: Vec, +} diff --git a/crates/flow-core/src/errors.rs b/crates/flow-core/src/errors.rs new file mode 100644 index 0000000..f4739e7 --- /dev/null +++ b/crates/flow-core/src/errors.rs @@ -0,0 +1,239 @@ +//! Error types for flow-core operations. +//! +//! This module defines the error types used throughout the crate. +//! Errors use [`miette`] for rich diagnostic output with error codes, +//! help text, and source code context. +//! +//! # Overview +//! +//! The [`Error`] enum represents all possible errors that can occur when +//! working with Flow spaces and filesystems. Each variant includes: +//! +//! - A human-readable error message +//! - A unique error code (e.g., `flow::io_error`) +//! - Contextual help text to guide users toward a solution +//! +//! # Error Handling +//! +//! All errors in this crate are compatible with [`miette`]'s diagnostic +//! system, which provides rich error reporting in CLI applications. +//! +//! # Examples +//! +//! ``` +//! use flow_core::Error; +//! use std::path::PathBuf; +//! +//! // Errors can be created directly +//! let error = Error::NotFound(PathBuf::from("/nonexistent/path")); +//! +//! // They implement Display for human-readable messages +//! println!("Error: {}", error); +//! ``` + +use std::path::PathBuf; + +use miette::Diagnostic; +use thiserror::Error; + +use crate::space::Locator; + +/// Errors that can occur when working with spaces. +/// +/// This enum covers all error conditions that may arise during space +/// initialization, loading, and filesystem operations. Each variant +/// provides detailed diagnostic information through [`miette`]. +/// +/// # Variants +/// +/// | Variant | Error Code | Description | +/// |---------|------------|-------------| +/// | [`Io`](Error::Io) | `flow::io_error` | Low-level filesystem errors | +/// | [`NotFound`](Error::NotFound) | `flow::path_not_found` | Path does not exist | +/// | [`NotADirectory`](Error::NotADirectory) | `flow::not_a_directory` | Path is not a directory | +/// | [`AlreadyExists`](Error::AlreadyExists) | `flow::already_exists` | Space already exists | +/// | [`DirectoryNotEmpty`](Error::DirectoryNotEmpty) | `flow::directory_not_empty` | Directory has contents | +/// +/// # Examples +/// +/// Matching on specific error variants: +/// +/// ``` +/// use flow_core::Error; +/// use std::path::PathBuf; +/// +/// fn handle_error(error: Error) { +/// match error { +/// Error::NotFound(path) => { +/// eprintln!("Path not found: {}", path.display()); +/// } +/// Error::AlreadyExists(path) => { +/// eprintln!("Space already exists at: {}", path.display()); +/// } +/// _ => eprintln!("An error occurred: {}", error), +/// } +/// } +/// ``` +#[derive(Debug, Error, Diagnostic)] +pub enum Error { + /// A filesystem operation failed. + /// + /// This variant wraps low-level I/O errors from the operating system, + /// such as permission denied, disk full, or network errors when + /// accessing remote filesystems. + /// + /// # Error Code + /// + /// `flow::io_error` + /// + /// # Examples + /// + /// ``` + /// use flow_core::Error; + /// use std::io; + /// + /// let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied"); + /// let error: Error = io_error.into(); + /// ``` + #[error("Filesystem error: {0}")] + #[diagnostic(code(flow::io_error))] + Io(#[from] std::io::Error), + + /// The specified path does not exist. + /// + /// This error occurs when attempting to initialize or load a space + /// from a path that does not exist on the filesystem. + /// + /// # Error Code + /// + /// `flow::path_not_found` + /// + /// # Fields + /// + /// * `0` - The path that was not found. + #[error("Path does not exist: {0}")] + #[diagnostic( + code(flow::path_not_found), + url(docsrs), + help("Make sure the directory exists before initializing a space") + )] + NotFound(PathBuf), + + /// A space already exists at the specified path. + /// + /// This error occurs when attempting to initialize a new space in + /// a directory that already contains a `.flow` directory, indicating + /// an existing space. + /// + /// # Error Code + /// + /// `flow::already_exists` + /// + /// # Fields + /// + /// * `0` - The path where the space already exists. + #[error("A space already exists at: {0}")] + #[diagnostic( + code(flow::already_exists), + url(docsrs), + help("Use `flow open` to open the existing space, or choose a different path") + )] + AlreadyExists(PathBuf), + + /// The specified path is not a directory. + /// + /// This error occurs when a path exists but is a file rather than + /// a directory. Spaces can only be initialized in directories. + /// + /// # Error Code + /// + /// `flow::not_a_directory` + /// + /// # Fields + /// + /// * `0` - The path that is not a directory. + #[error("Path is not a directory: {0}")] + #[diagnostic(code(flow::not_a_directory), url(docsrs), help("Make sure the path is a directory"))] + NotADirectory(PathBuf), + + /// The directory is not empty. + /// + /// This error occurs when attempting to initialize a space in a + /// directory that contains files or subdirectories. Spaces should + /// be initialized in empty directories to avoid conflicts. + /// + /// # Error Code + /// + /// `flow::directory_not_empty` + /// + /// # Fields + /// + /// * `0` - The path to the non-empty directory. + #[error("Directory is not empty: {0}")] + #[diagnostic( + code(flow::directory_not_empty), + url(docsrs), + help("Initialize a space in an empty directory, or use a different path") + )] + DirectoryNotEmpty(PathBuf), + + /// A space with the given name is already registered. + /// + /// This error occurs when attempting to register a space with a name + /// that is already in use by another registered space. + /// + /// # Error Code + /// + /// `flow::space_already_registered` + /// + /// # Fields + /// + /// * `0` - The name that is already registered. + #[error("A space with the name '{0}' is already registered")] + #[diagnostic( + code(flow::space_already_registered), + url(docsrs), + help("Use a different name, or unregister the existing space first") + )] + SpaceAlreadyRegistered(String), + + /// A space at the given path is already registered. + /// + /// This error occurs when attempting to register a space at a path + /// that is already registered under a different name. + /// + /// # Error Code + /// + /// `flow::space_path_already_registered` + /// + /// # Fields + /// + /// * `0` - The path that is already registered. + #[error("A space at path '{0}' is already registered")] + #[diagnostic( + code(flow::space_path_already_registered), + url(docsrs), + help("This space is already registered under a different name") + )] + SpacePathAlreadyRegistered(PathBuf), + + /// The specified space is not registered. + /// + /// This error occurs when attempting to operate on a space that + /// has not been registered in the configuration. + /// + /// # Error Code + /// + /// `flow::space_not_registered` + /// + /// # Fields + /// + /// * `0` - The locator used to find the space. + #[error("Space not registered: {0}")] + #[diagnostic( + code(flow::space_not_registered), + url(docsrs), + help("Register the space first with `flow register`") + )] + SpaceNotRegistered(Locator), +} diff --git a/crates/flow-core/src/filesystem.rs b/crates/flow-core/src/filesystem.rs new file mode 100644 index 0000000..f14c953 --- /dev/null +++ b/crates/flow-core/src/filesystem.rs @@ -0,0 +1,57 @@ +//! Filesystem abstraction layer for Flow. +//! +//! This module provides a trait-based abstraction over filesystem operations, +//! allowing for different implementations such as local filesystem access, +//! in-memory filesystems for testing, or remote storage backends. +//! +//! # Design +//! +//! The [`Filesystem`] trait defines the core operations needed by Flow: +//! reading and writing files. By abstracting these operations behind a trait, +//! we gain several benefits: +//! +//! - **Testability**: Tests can use an in-memory implementation to avoid +//! touching the real filesystem. +//! - **Flexibility**: Future implementations could support cloud storage, +//! encrypted filesystems, or other backends. +//! - **Isolation**: The core logic doesn't depend on `std::fs` directly. +//! +//! # Implementations +//! +//! This module provides the following implementations of the [`Filesystem`] trait: +//! +//! - [`LocalFilesystem`] - The production implementation that delegates to +//! Tokio's async filesystem APIs for local file I/O. +//! +//! # Re-exports +//! +//! The following types are re-exported for convenience: +//! +//! - [`Filesystem`] - The core trait defining filesystem operations. +//! - [`LocalFilesystem`] - The local filesystem implementation. +//! +//! # Examples +//! +//! ```ignore +//! use std::path::Path; +//! use flow_core::filesystem::{Filesystem, LocalFilesystem}; +//! +//! # async fn example() -> miette::Result<()> { +//! let fs = LocalFilesystem; +//! +//! // Write a file +//! fs.write(Path::new("notes/todo.md"), "# TODO\n- Buy milk").await?; +//! +//! // Read it back +//! let content = fs.read(Path::new("notes/todo.md")).await?; +//! let text = String::from_utf8(content).expect("valid UTF-8"); +//! assert!(text.contains("Buy milk")); +//! # Ok(()) +//! # } +//! ``` + +mod local; +mod traits; + +pub use self::local::LocalFilesystem; +pub use self::traits::Filesystem; diff --git a/crates/flow-core/src/filesystem/local.rs b/crates/flow-core/src/filesystem/local.rs new file mode 100644 index 0000000..b4fef91 --- /dev/null +++ b/crates/flow-core/src/filesystem/local.rs @@ -0,0 +1,130 @@ +//! Local filesystem implementation. +//! +//! This module provides [`LocalFilesystem`], the production implementation +//! of the [`Filesystem`](super::Filesystem) trait that delegates to +//! Tokio's async filesystem APIs. + +use std::path::Path; + +use miette::Result; +use tokio::fs::{create_dir, create_dir_all, metadata, read, read_dir, read_to_string, try_exists, write}; + +use crate::errors::Error; +use crate::filesystem::traits::Filesystem; + +/// A [`Filesystem`] implementation that operates on the local filesystem. +/// +/// This is the standard implementation used in production. It delegates +/// to the operating system's filesystem APIs via Tokio's async filesystem +/// operations. +/// +/// # Examples +/// +/// ```ignore +/// use std::path::Path; +/// use flow_core::filesystem::{Filesystem, LocalFilesystem}; +/// +/// # async fn example() -> miette::Result<()> { +/// let fs = LocalFilesystem; +/// +/// // Check if a file exists +/// if fs.exists(Path::new("notes/todo.md")).await? { +/// let content = fs.read(Path::new("notes/todo.md")).await?; +/// println!("File has {} bytes", content.len()); +/// } +/// +/// // Write a new file +/// fs.write(Path::new("notes/new.md"), b"# New Note").await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Default, Clone, Copy)] +pub struct LocalFilesystem; + +impl Filesystem for LocalFilesystem { + /// Checks if a path exists using [`tokio::fs::try_exists`]. + /// + /// This is a non-blocking operation that queries the filesystem + /// asynchronously. + async fn exists(&self, path: impl AsRef + Send + Sync) -> Result { + try_exists(&path).await.map_err(|e| Error::Io(e).into()) + } + + /// Checks if a path is a directory using [`tokio::fs::metadata`]. + /// + /// Returns an error if the path does not exist, unlike [`exists`](Self::exists) + /// which returns `false` for non-existent paths. + async fn is_dir(&self, path: impl AsRef + Send + Sync) -> Result { + let metadata = metadata(&path).await.map_err(Error::Io)?; + + Ok(metadata.is_dir()) + } + + /// Checks if a directory is empty by attempting to read its first entry. + /// + /// Uses [`tokio::fs::read_dir`] internally. Returns `false` if the path + /// is not a directory (does not error). + async fn is_dir_empty(&self, path: impl AsRef + Send + Sync) -> Result { + if !self.is_dir(&path).await? { + return Ok(false); + } + + let mut entries = read_dir(&path).await.map_err(Error::Io)?; + let is_empty = entries.next_entry().await.map_err(Error::Io)?.is_none(); + + Ok(is_empty) + } + + /// Creates a directory using [`tokio::fs::create_dir`]. + /// + /// This does **not** create parent directories. Use this only when + /// the parent directory is known to exist. + async fn create_dir(&self, path: impl AsRef + Send + Sync) -> Result<()> { + create_dir(&path).await.map_err(|e| Error::Io(e).into()) + } + + /// Creates a directory using [`tokio::fs::create_dir_all`]. + /// + /// This does create parent directories. Use this only when + /// the parent directory is known to exist. + async fn create_dir_all(&self, path: impl AsRef + Send + Sync) -> Result<()> { + create_dir_all(&path).await.map_err(|e| Error::Io(e).into()) + } + + /// Writes content to a file using [`tokio::fs::write`]. + /// + /// This operation is atomic on most platforms — the file is either + /// fully written or not modified at all. + async fn write( + &self, + path: impl AsRef + Send + Sync, + contents: impl AsRef<[u8]> + Send + Sync, + ) -> Result<()> { + write(&path, &contents) + .await + .map_err(|e| Error::Io(e).into()) + } + + /// Reads the entire file contents using [`tokio::fs::read`]. + /// + /// The entire file is read into memory. For large files, consider + /// using streaming APIs instead. + async fn read(&self, path: impl AsRef + Send + Sync) -> Result> { + let path = path.as_ref(); + read(path).await.map_err(|e| Error::Io(e).into()) + } + + /// Reads the entire file contents using [`tokio::fs::read_to_string`]. + /// + /// The entire file is read into memory. For large files, consider + /// using streaming APIs instead. + async fn read_to_string(&self, path: impl AsRef + Send + Sync) -> Result { + let path = path.as_ref(); + read_to_string(path).await.map_err(|e| Error::Io(e).into()) + } +} + +#[cfg(test)] +mod tests { + // TODO: Add tests with a mock filesystem implementation +} diff --git a/crates/flow-core/src/filesystem/traits.rs b/crates/flow-core/src/filesystem/traits.rs new file mode 100644 index 0000000..a4c733e --- /dev/null +++ b/crates/flow-core/src/filesystem/traits.rs @@ -0,0 +1,143 @@ +use miette::Result; +use std::path::Path; + +/// An abstraction over filesystem operations. +/// +/// This trait defines the minimal set of operations needed for Flow to +/// interact with a filesystem. Implementations must be thread-safe +/// (`Send + Sync`) to support async operations across threads. +/// +/// # Implementors +/// +/// - [`LocalFilesystem`] - Reads and writes to the local filesystem. +/// +/// # Examples +/// +/// Using the filesystem trait with dependency injection: +/// +/// ```ignore +/// struct NoteStore { +/// fs: F, +/// root: PathBuf, +/// } +/// +/// impl NoteStore { +/// async fn save_note(&self, name: &str, content: &str) -> Result<()> { +/// let path = self.root.join(name).with_extension("md"); +/// self.fs.write(&path, content).await +/// } +/// } +/// ``` +pub trait Filesystem: Send + Sync { + /// Checks if a path exists on the filesystem. + /// + /// Returns `true` if the path exists (file or directory), `false` otherwise. + /// + /// # Arguments + /// + /// * `path` - The path to check for existence. + /// + /// # Errors + /// + /// Returns an error if the filesystem cannot be queried (e.g., permission denied). + async fn exists(&self, path: impl AsRef + Send + Sync) -> Result; + + /// Checks if a path is a directory. + /// + /// Returns `true` if the path exists and is a directory, `false` otherwise. + /// + /// # Arguments + /// + /// * `path` - The path to check. + /// + /// # Errors + /// + /// Returns an error if the path does not exist or cannot be queried. + async fn is_dir(&self, path: impl AsRef + Send + Sync) -> Result; + + /// Checks if a directory is empty. + /// + /// Returns `true` if the path is a directory with no entries, `false` otherwise. + /// Returns `false` if the path is not a directory. + /// + /// # Arguments + /// + /// * `path` - The directory path to check. + /// + /// # Errors + /// + /// Returns an error if the directory cannot be read (e.g., permission denied). + async fn is_dir_empty(&self, path: impl AsRef + Send + Sync) -> Result; + + /// Creates a new directory at the given path. + /// + /// The parent directory must already exist. This does not create parent directories. + /// + /// # Arguments + /// + /// * `path` - The path where the directory should be created. + /// + /// # Errors + /// + /// Returns an error if: + /// - The parent directory does not exist. + /// - A file or directory already exists at the path. + /// - Permission is denied. + async fn create_dir(&self, path: impl AsRef + Send + Sync) -> Result<()>; + + /// Creates a new directory at the given path. + /// + /// The parent directory must already exist. This does create parent directories. + /// + /// # Arguments + /// + /// * `path` - The path where the directory should be created. + /// + /// # Errors + /// + /// Returns an error if: + /// - The parent directory does not exist. + /// - A file or directory already exists at the path. + /// - Permission is denied. + async fn create_dir_all(&self, path: impl AsRef + Send + Sync) -> Result<()>; + + /// Writes content to a file at the given path. + /// + /// Creates the file if it doesn't exist, or overwrites it if it does. + /// + /// # Arguments + /// + /// * `path` - The path to the file to write. + /// * `contents` - The byte content to write to the file. + /// + /// # Errors + /// + /// Returns an error if: + /// - The parent directory does not exist. + /// - Permission is denied. + /// - The disk is full or another I/O error occurs. + async fn write(&self, path: impl AsRef + Send + Sync, contents: impl AsRef<[u8]> + Send + Sync) + -> Result<()>; + + /// Reads the entire contents of a file into a byte vector. + /// + /// # Arguments + /// + /// * `path` - The path to the file to read. + /// + /// # Errors + /// + /// Returns an error if the file does not exist or cannot be read. + async fn read(&self, path: impl AsRef + Send + Sync) -> Result>; + + /// Reads the entire contents of a file into a string. + /// + /// # Arguments + /// + /// * `path` - The path to the file to read. + /// + /// # Errors + /// + /// Returns an error if the file does not exist or cannot be read. + async fn read_to_string(&self, path: impl AsRef + Send + Sync) -> Result; +} diff --git a/crates/flow-core/src/lib.rs b/crates/flow-core/src/lib.rs index 44bf58f..c1e25ad 100644 --- a/crates/flow-core/src/lib.rs +++ b/crates/flow-core/src/lib.rs @@ -1,16 +1,101 @@ -/// Adds two numbers together. -#[must_use] -pub const fn add(left: u64, right: u64) -> u64 { - left + right -} +//! # flow-core +//! +//! Core abstractions for the Flow notes and outliner system. +//! +//! This crate provides the fundamental building blocks for managing +//! notes, spaces, and file system operations. It is designed to be +//! the foundation that other Flow crates build upon. +//! +//! ## Key Concepts +//! +//! - **[`Space`]** - A workspace containing notes, configuration, and metadata. +//! Spaces are the top-level organizational unit in Flow. +//! +//! - **[`Locator`]** - A flexible way to identify spaces, either by their +//! human-readable name or by an explicit filesystem path. +//! +//! - **`Filesystem`** - An abstraction over file I/O operations, enabling +//! testability and potential future support for different storage backends. +//! +//! - **[`Error`]** - Rich error types with diagnostic information for +//! user-friendly error reporting. +//! +//! ## Examples +//! +//! ### Creating a new space +//! +//! ```no_run +//! use std::path::Path; +//! use flow_core::Space; +//! +//! # async fn example() -> miette::Result<()> { +//! let space = Space::init(Path::new("./my-notes"), "personal").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Loading an existing space +//! +//! ```no_run +//! use flow_core::Space; +//! +//! # async fn example() -> miette::Result<()> { +//! // Load by name (resolved from configuration) +//! let space = Space::load("personal").await?; +//! +//! // Or load by explicit path +//! let space = Space::load(std::path::PathBuf::from("./my-notes")).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Error handling +//! +//! ```no_run +//! use std::path::Path; +//! use flow_core::{Space, Error}; +//! +//! # async fn example() -> miette::Result<()> { +//! match Space::init(Path::new("./my-notes"), "personal").await { +//! Ok(space) => println!("Space created successfully!"), +//! Err(report) => { +//! // Errors are miette::Report, providing rich diagnostics +//! eprintln!("Failed to create space: {:?}", report); +//! } +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Module Structure +//! +//! - `space` - Space management, including initialization and loading. +//! - `filesystem` - Filesystem abstraction layer for I/O operations. +//! - `errors` - Error types with rich diagnostic information. +//! +//! ## Feature Flags +//! +//! This crate does not currently define any feature flags. +//! +//! ## Re-exports +//! +//! For convenience, the most commonly used types are re-exported at the +//! crate root: +//! +//! - [`Space`] - The main space type. +//! - [`Locator`] - For identifying spaces by name or path. +//! - [`Error`] - The error type for this crate. +//! +//! [`Space`]: space::Space +//! [`Locator`]: space::Locator -#[cfg(test)] -mod tests { - use super::*; +mod filesystem; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +mod config; +mod errors; +mod space; + +pub use self::config::Config; +pub use self::errors::Error; +pub use self::space::Locator; +pub use self::space::Space; diff --git a/crates/flow-core/src/space.rs b/crates/flow-core/src/space.rs new file mode 100644 index 0000000..c78c8a1 --- /dev/null +++ b/crates/flow-core/src/space.rs @@ -0,0 +1,245 @@ +//! Space management for Flow. +//! +//! A [`Space`] is the primary organizational unit in Flow — it represents +//! a workspace containing notes, configuration, and metadata. Think of it +//! as a "vault" or "notebook" that groups related content together. +//! +//! # Overview +//! +//! Spaces can be: +//! - **Initialized** at a new location with [`Space::init`] +//! - **Loaded** from an existing location with [`Space::load`] +//! +//! Each space has a human-readable name and lives at a specific filesystem +//! path. Spaces can be located either by name (resolved from global +//! configuration) or by explicit path using a [`Locator`]. +//! +//! # Examples +//! +//! ## Creating a new space +//! +//! ```no_run +//! use std::path::Path; +//! use flow_core::Space; +//! +//! # async fn example() -> miette::Result<()> { +//! // Initialize a new space called "personal" at the given path +//! let space = Space::init(Path::new("./my-notes"), "personal").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Loading an existing space +//! +//! ```no_run +//! use flow_core::Space; +//! +//! # async fn example() -> miette::Result<()> { +//! // Load by name (resolved from global configuration) +//! let space = Space::load("personal").await?; +//! +//! // Or load by explicit path +//! let space = Space::load(std::path::PathBuf::from("./my-notes")).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Architecture +//! +//! The public [`Space`] struct is a thin wrapper around an internal +//! implementation (`DefaultSpace`). This design +//! allows us to: +//! +//! - Keep the public API simple and stable +//! - Inject different filesystem implementations for testing +//! - Potentially support different space backends in the future + +use std::path::Path; + +use miette::Result; + +use crate::filesystem::LocalFilesystem; +use crate::space::default::DefaultSpace; +use crate::space::traits::Space as _; + +mod default; +mod locator; +mod metadata; +mod traits; + +pub use self::locator::Locator; +pub use self::metadata::Metadata; + +/// A Flow workspace containing notes, configuration, and metadata. +/// +/// `Space` is the main entry point for interacting with a Flow workspace. +/// It provides methods to initialize new spaces and load existing ones. +/// +/// # Examples +/// +/// ```no_run +/// use std::path::Path; +/// use flow_core::Space; +/// +/// # async fn example() -> miette::Result<()> { +/// // Create a new space +/// let space = Space::init(Path::new("./notes"), "work").await?; +/// +/// // Later, load it by name +/// let space = Space::load("work").await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Thread Safety +/// +/// `Space` is `Send` and `Sync`, making it safe to share across threads +/// and use in async contexts. +pub struct Space { + /// The underlying space implementation. + #[allow(dead_code)] + inner: DefaultSpace, +} + +impl Space { + /// Initialize a new space at the given path. + /// + /// This creates the necessary directory structure and configuration + /// files for a new Flow space. The space will be registered with the + /// given name, allowing it to be loaded by name in the future. + /// + /// # Arguments + /// + /// * `path` - The directory path where the space will be created. + /// The directory will be created if it doesn't exist. + /// * `name` - A human-readable name for the space. This name can be + /// used later with [`Space::load`] to open the space. + /// + /// # Errors + /// + /// Returns an error if: + /// - A space already exists at the given path + /// - A space with the given name is already registered + /// - The directory cannot be created (e.g., permission denied) + /// - The configuration file cannot be written + /// + /// # Examples + /// + /// ```no_run + /// use std::path::Path; + /// use flow_core::Space; + /// + /// # async fn example() -> miette::Result<()> { + /// let space = Space::init(Path::new("./my-notes"), "personal").await?; + /// println!("Space created successfully!"); + /// # Ok(()) + /// # } + /// ``` + #[must_use = "Space was initialized but never used"] + pub async fn init(path: impl AsRef + Send + Sync, name: impl Into) -> Result { + let fs = LocalFilesystem; + let inner = DefaultSpace::init(fs, path, name).await?; + + Ok(Self { inner }) + } + + /// Load an existing space. + /// + /// Spaces can be loaded either by their registered name or by an + /// explicit filesystem path. The `locator` parameter accepts anything + /// that can be converted into a [`Locator`], including: + /// + /// - `&str` or `String` — interpreted as a space name + /// - `&Path` or `PathBuf` — interpreted as a filesystem path + /// + /// # Arguments + /// + /// * `locator` - Identifies which space to load. See [`Locator`] for + /// the different ways to specify a space. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space cannot be found (name not registered or path doesn't exist) + /// - The space configuration is missing or corrupted + /// - The filesystem cannot be read (e.g., permission denied) + /// + /// # Examples + /// + /// ```no_run + /// use flow_core::Space; + /// use std::path::PathBuf; + /// + /// # async fn example() -> miette::Result<()> { + /// // Load by name + /// let space = Space::load("personal").await?; + /// + /// // Load by path + /// let space = Space::load(PathBuf::from("./my-notes")).await?; + /// # Ok(()) + /// # } + /// ``` + #[must_use = "Space was loaded but never used"] + pub async fn load(locator: impl Into) -> Result { + let fs = LocalFilesystem; + let inner = DefaultSpace::load(fs, locator.into()).await?; + + Ok(Self { inner }) + } + + /// Returns the name of the space. + /// + /// The name is a human-readable identifier for the space, set during + /// initialization. It is used for display purposes and can be used + /// to look up the space in the registry. + /// + /// # Examples + /// + /// ```no_run + /// use std::path::Path; + /// use flow_core::Space; + /// + /// # async fn example() -> miette::Result<()> { + /// let space = Space::init(Path::new("./my-notes"), "personal").await?; + /// assert_eq!(space.name(), "personal"); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn name(&self) -> &str { + self.inner.name() + } + + /// Returns the filesystem path to the space directory. + /// + /// This is the root directory containing the `.flow/` subdirectory + /// and all space content. + /// + /// # Examples + /// + /// ```no_run + /// use std::path::Path; + /// use flow_core::Space; + /// + /// # async fn example() -> miette::Result<()> { + /// let space = Space::init(Path::new("./my-notes"), "personal").await?; + /// assert_eq!(space.path(), Path::new("./my-notes")); + /// # Ok(()) + /// # } + /// ``` + #[must_use] + pub fn path(&self) -> &Path { + self.inner.path() + } +} + +#[cfg(test)] +mod tests { + // TODO: Add integration tests for Space + // + // Tests should cover: + // - Initializing a new space + // - Loading an existing space by name + // - Loading an existing space by path + // - Error cases (space not found, already exists, etc.) +} diff --git a/crates/flow-core/src/space/default.rs b/crates/flow-core/src/space/default.rs new file mode 100644 index 0000000..0382483 --- /dev/null +++ b/crates/flow-core/src/space/default.rs @@ -0,0 +1,260 @@ +//! Default implementation of the [`Space`] trait. +//! +//! This module provides [`DefaultSpace`], the standard implementation of +//! the space trait used in production. It is generic over the filesystem +//! implementation to allow for dependency injection during testing. +//! +//! # Overview +//! +//! The [`DefaultSpace`] struct manages the lifecycle of a Flow space, +//! including: +//! +//! - Creating the directory structure (`.flow/`, `journal/`) +//! - Writing and reading space metadata (`space.json`) +//! - Managing the Loro CRDT document for collaboration +//! +//! # Directory Structure +//! +//! When a space is initialized, the following structure is created: +//! +//! ```text +//! my-space/ +//! ├── .flow/ +//! │ ├── space.json # Space metadata +//! │ └── space.loro # Loro CRDT document snapshot +//! └── journal/ # Directory for journal entries +//! ``` +//! +//! # Examples +//! +//! ```ignore +//! use flow_core::filesystem::LocalFilesystem; +//! use flow_core::space::default::DefaultSpace; +//! use flow_core::space::traits::Space; +//! +//! // Initialize a new space +//! let fs = LocalFilesystem; +//! let space = DefaultSpace::init(fs, "./my-space", "personal").await?; +//! ``` + +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use loro::LoroDoc; +use miette::{ensure, IntoDiagnostic, Result}; + +use crate::{ + errors::Error, + filesystem::Filesystem, + space::{traits::Space, Locator, Metadata}, +}; + +/// The directory name where Flow stores space metadata. +/// +/// This hidden directory contains all Flow-specific files, keeping +/// the user's content directory clean. +const FLOW_DIR: &str = ".flow"; + +/// The filename for space metadata (JSON format). +/// +/// This file stores the space name, version, and other metadata +/// needed to identify and manage the space. +const METADATA_FILE: &str = "space.json"; + +/// The filename for the Loro CRDT document snapshot. +/// +/// This file contains a binary snapshot of the Loro CRDT document, +/// which enables offline-first collaboration and conflict resolution. +const DOCUMENT_FILE: &str = "space.loro"; + +/// The directory name for journal entries. +/// +/// Journal entries are stored as individual files in this directory, +/// organized by date or other criteria. +const JOURNAL_DIR: &str = "journal"; + +/// The default implementation of a Flow space. +/// +/// `DefaultSpace` is generic over the filesystem implementation, allowing +/// different storage backends to be used. In production, this is typically +/// [`LocalFilesystem`](crate::filesystem::LocalFilesystem), while tests +/// can inject mock implementations. +/// +/// # Type Parameters +/// +/// * `F` - The filesystem implementation to use for all I/O operations. +/// Must implement [`Filesystem`] and be thread-safe (`Send + Sync`). +/// +/// # Fields +/// +/// * `fs` - The filesystem backend for all I/O operations. +/// * `metadata` - Space metadata including name and version. +/// * `doc` - The Loro CRDT document for collaborative editing. +/// +/// # Thread Safety +/// +/// `DefaultSpace` is `Send` and `Sync` when the filesystem implementation +/// is also `Send` and `Sync`, making it safe for use in async contexts. +/// +/// # Examples +/// +/// Initializing a new space: +/// +/// ```ignore +/// use flow_core::filesystem::LocalFilesystem; +/// use flow_core::space::default::DefaultSpace; +/// use flow_core::space::traits::Space; +/// +/// let fs = LocalFilesystem; +/// let space = DefaultSpace::init(fs, "./my-space", "personal").await?; +/// ``` +/// +/// Using a mock filesystem for testing: +/// +/// ```ignore +/// use flow_core::space::default::DefaultSpace; +/// use flow_core::space::traits::Space; +/// +/// let mock_fs = MockFilesystem::new(); +/// let space = DefaultSpace::init(mock_fs, "./test-space", "test").await?; +/// ``` +#[allow(dead_code)] +pub struct DefaultSpace { + /// The filesystem implementation used for all I/O operations. + /// + /// This is injected at construction time, allowing different backends + /// to be used in production vs. testing environments. + fs: F, + + /// Path of the space. + path: PathBuf, + + /// Metadata of the space. + /// + /// Contains the space name, version, and other identifying information. + /// This is persisted to `.flow/space.json`. + metadata: Metadata, + + /// Loro CRDT document for offline and local-first collaboration. + /// + /// This document stores the space's content in a format that supports + /// conflict-free merging, enabling offline editing and real-time + /// collaboration. + doc: LoroDoc, +} + +impl Space for DefaultSpace { + type Fs = F; + + /// Initializes a new space at the given path. + /// + /// This implementation performs the following steps: + /// + /// 1. Validates that the path exists and is an empty directory + /// 2. Creates the `.flow/` directory for metadata storage + /// 3. Creates the `journal/` directory for journal entries + /// 4. Writes the space metadata to `.flow/space.json` + /// 5. Creates and persists an empty Loro CRDT document + /// + /// # Implementation Notes + /// + /// - The space metadata is serialized as JSON using [`serde_json`]. + /// - The Loro document is exported as a binary snapshot for efficiency. + /// - All filesystem operations use the injected `fs` implementation. + async fn init(fs: Self::Fs, path: impl AsRef + Send + Sync, name: impl Into) -> Result { + let path = path.as_ref(); + let exists = fs.exists(path).await?; + if exists { + let is_dir = fs.is_dir(path).await?; + ensure!(is_dir, Error::NotADirectory(path.to_path_buf())); + } else { + fs.create_dir_all(path).await?; + } + + let flow_dir = path.join(FLOW_DIR); + let is_empty = fs.is_dir_empty(path).await?; + if !is_empty { + let has_space = fs.exists(&flow_dir).await?; + if has_space { + return Err(Error::AlreadyExists(path.to_path_buf()).into()); + } + + return Err(Error::DirectoryNotEmpty(path.to_path_buf()).into()); + } + + let journal_dir = path.join(JOURNAL_DIR); + fs.create_dir(&flow_dir).await?; + fs.create_dir(&journal_dir).await?; + + let metadata = Metadata { + name: name.into(), + version: Cow::Borrowed(env!("CARGO_PKG_VERSION")), + }; + let metadata_json = serde_json::to_string_pretty(&metadata).into_diagnostic()?; + let metadata_path = flow_dir.join(METADATA_FILE); + fs.write(&metadata_path, metadata_json.as_bytes()).await?; + + let doc = LoroDoc::new(); + let doc_snapshot = doc.export(loro::ExportMode::Snapshot).into_diagnostic()?; + let doc_path = flow_dir.join(DOCUMENT_FILE); + fs.write(&doc_path, &doc_snapshot).await?; + + Ok(Self { + fs, + path: path.to_path_buf(), + metadata, + doc, + }) + } + + /// Loads an existing space from the given locator. + /// + /// # Implementation Notes + /// + /// This method will: + /// + /// 1. Resolve the [`Locator`] to a filesystem path + /// 2. Read and deserialize the space metadata from `.flow/space.json` + /// 3. Load the Loro CRDT document from `.flow/space.loro` + /// + /// # Unimplemented + /// + /// This method is not yet implemented and will panic if called. + async fn load(fs: Self::Fs, locator: Locator) -> Result { + let path = match locator { + Locator::Name(_name) => todo!("Look up the path through the name of the space"), + Locator::Path(path) => path, + }; + + let flow_dir = path.join(FLOW_DIR); + let metadata_path = flow_dir.join(METADATA_FILE); + let metadata_json = fs.read_to_string(&metadata_path).await?; + let metadata = serde_json::from_str::(&metadata_json).into_diagnostic()?; // TODO: Create custom error for this? + + let doc_path = flow_dir.join(DOCUMENT_FILE); + let doc_snapshot = fs.read(&doc_path).await?; + let doc = LoroDoc::from_snapshot(&doc_snapshot).into_diagnostic()?; // TODO: Create custom error for this? + + Ok(Self { + fs, + path, + metadata, + doc, + }) + } + + fn name(&self) -> &str { + &self.metadata.name + } + + fn path(&self) -> &Path { + &self.path + } +} + +#[cfg(test)] +mod tests { + // TODO: Add tests with a mock filesystem implementation +} diff --git a/crates/flow-core/src/space/locator.rs b/crates/flow-core/src/space/locator.rs new file mode 100644 index 0000000..66a070c --- /dev/null +++ b/crates/flow-core/src/space/locator.rs @@ -0,0 +1,115 @@ +//! Space locator types. +//! +//! This module provides [`Locator`], a flexible way to identify spaces +//! either by name or by filesystem path. +//! +//! # Overview +//! +//! When working with Flow spaces, you often need to specify which space +//! to operate on. The [`Locator`] enum provides two ways to do this: +//! +//! - **By name**: Use a human-readable name that is resolved from the +//! global Flow configuration (e.g., `"personal"`, `"work"`). +//! - **By path**: Use an explicit filesystem path to the space directory. +//! +//! # Type Conversions +//! +//! `Locator` implements [`From`] for common string and path types, making +//! it easy to use with APIs that accept `impl Into`: +//! +//! - `String` and `&str` are interpreted as space names. +//! - `PathBuf` and `&Path` are interpreted as filesystem paths. + +use std::fmt; +use std::path::{Path, PathBuf}; + +/// A way to locate a [`Space`](super::Space). +/// +/// Spaces can be identified either by their human-readable name +/// (which is resolved from a configuration directory) or by +/// their explicit filesystem path. +/// +/// # Examples +/// +/// ``` +/// use flow_core::Locator; +/// use std::path::PathBuf; +/// +/// // From a string (interpreted as name) +/// let loc: Locator = "my-notes".into(); +/// +/// // From a PathBuf (interpreted as path) +/// let loc: Locator = PathBuf::from("/home/user/notes").into(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Locator { + /// Locate a space by its registered name. + /// + /// The name is resolved from the global Flow configuration file, + /// which maps names to filesystem paths. This is the preferred + /// way to reference spaces in most user-facing contexts. + /// + /// # Examples + /// + /// ``` + /// use flow_core::Locator; + /// + /// let locator = Locator::Name("personal".to_string()); + /// ``` + Name(String), + + /// Locate a space by its explicit filesystem path. + /// + /// This bypasses the global configuration and directly references + /// a space directory. Useful for working with spaces that haven't + /// been registered, or for tools that operate on arbitrary paths. + /// + /// # Examples + /// + /// ``` + /// use std::path::PathBuf; + /// use flow_core::Locator; + /// + /// let locator = Locator::Path(PathBuf::from("/home/user/notes")); + /// ``` + Path(PathBuf), +} + +impl From for Locator { + fn from(s: String) -> Self { + Self::Name(s) + } +} + +impl From<&String> for Locator { + fn from(s: &String) -> Self { + Self::Name(s.clone()) + } +} + +impl From<&str> for Locator { + fn from(s: &str) -> Self { + Self::Name(s.to_owned()) + } +} + +impl From for Locator { + fn from(path: PathBuf) -> Self { + Self::Path(path) + } +} + +impl From<&Path> for Locator { + fn from(path: &Path) -> Self { + Self::Path(path.to_path_buf()) + } +} + +impl fmt::Display for Locator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Name(name) => write!(f, "{name}"), + Self::Path(path) => write!(f, "{}", path.display()), + } + } +} diff --git a/crates/flow-core/src/space/metadata.rs b/crates/flow-core/src/space/metadata.rs new file mode 100644 index 0000000..98a3d60 --- /dev/null +++ b/crates/flow-core/src/space/metadata.rs @@ -0,0 +1,82 @@ +//! Space metadata types. +//! +//! This module provides [`Metadata`], which contains information about +//! a space such as its name and the Flow version that created it. +//! +//! # Overview +//! +//! Every Flow space has an associated metadata file (`.flow/space.json`) +//! that stores essential information about the space. This metadata is +//! read when loading a space and written when initializing a new one. +//! +//! # Serialization +//! +//! The [`Metadata`] struct is serialized to JSON using [`serde`]. The +//! format is designed to be human-readable and forward-compatible, +//! allowing future versions of Flow to add new fields without breaking +//! existing spaces. + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +/// Metadata about a Flow space. +/// +/// This struct is serialized to JSON and stored in the `.flow/space.json` +/// file within each space. It contains essential information needed to +/// identify and manage the space. +/// +/// # Fields +/// +/// * `name` - The human-readable name of the space, used for identification. +/// * `version` - The Flow version that created or last migrated this space. +/// +/// # Serialization Format +/// +/// The metadata is stored as JSON: +/// +/// ```json +/// { +/// "name": "personal", +/// "version": "0.1.0" +/// } +/// ``` +/// +/// # Examples +/// +/// Creating metadata for a new space: +/// +/// ```ignore +/// use flow_core::space::Metadata; +/// +/// let metadata = Metadata { +/// name: "personal".to_string(), +/// version: "0.1.0".to_string(), +/// }; +/// ``` +/// +/// Serializing metadata to JSON: +/// +/// ```ignore +/// use flow_core::space::Metadata; +/// +/// let metadata = Metadata { +/// name: "work".to_string(), +/// version: "0.1.0".to_string(), +/// }; +/// +/// let json = serde_json::to_string_pretty(&metadata).unwrap(); +/// assert!(json.contains("\"name\": \"work\"")); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Metadata { + /// The human-readable name of the space. + /// + /// This name is used to identify the space in commands like `flow open `. + pub name: String, + + /// The version of Flow that created this space. + /// + /// This can be used for future migrations if the space format changes. + pub version: Cow<'static, str>, +} diff --git a/crates/flow-core/src/space/traits.rs b/crates/flow-core/src/space/traits.rs new file mode 100644 index 0000000..1245d9d --- /dev/null +++ b/crates/flow-core/src/space/traits.rs @@ -0,0 +1,142 @@ +//! Internal trait definitions for space implementations. +//! +//! This module contains the internal traits that define the contract +//! for space implementations. These traits are not part of the public API. + +use std::path::Path; + +use miette::Result; + +use crate::{filesystem::Filesystem, space::locator::Locator}; + +/// Internal trait defining the core operations for a space. +/// +/// This trait is implemented by [`DefaultSpace`] and potentially other +/// space implementations. It is not exposed publicly; instead, the public +/// [`Space`](super::Space) struct wraps implementations of this trait. +/// +/// # Design Notes +/// +/// The trait is generic over the filesystem implementation (`Fs`), which +/// enables dependency injection for testing. Production code uses +/// [`LocalFilesystem`](crate::filesystem::LocalFilesystem), while tests +/// can provide mock implementations. +/// +/// # Implementors +/// +/// - [`DefaultSpace`](super::default::DefaultSpace) - The standard implementation. +/// +/// # Examples +/// +/// Implementing a custom space type: +/// +/// ```ignore +/// use flow_core::filesystem::Filesystem; +/// use flow_core::space::{Locator, traits::Space}; +/// use miette::Result; +/// use std::path::Path; +/// +/// struct CustomSpace { +/// fs: F, +/// // ... other fields +/// } +/// +/// impl Space for CustomSpace { +/// type Fs = F; +/// +/// async fn init(fs: Self::Fs, path: impl AsRef, name: &str) -> Result { +/// // Custom initialization logic +/// todo!() +/// } +/// +/// async fn load(fs: Self::Fs, locator: Locator) -> Result { +/// // Custom loading logic +/// todo!() +/// } +/// } +/// ``` +pub trait Space: Sized + Send + Sync { + /// The filesystem implementation used by this space. + /// + /// This associated type allows space implementations to be generic + /// over different filesystem backends. The filesystem must implement + /// [`Filesystem`] and be thread-safe (`Send + Sync`). + /// + /// # Examples + /// + /// ```ignore + /// use flow_core::filesystem::LocalFilesystem; + /// + /// // In production, use LocalFilesystem + /// type Fs = LocalFilesystem; + /// + /// // In tests, use a mock filesystem + /// type Fs = MockFilesystem; + /// ``` + type Fs: Filesystem + Send + Sync; + + /// Initialize a new space at the given path. + /// + /// This creates the necessary directory structure and configuration + /// files for a new space. The space will be registered with the given + /// name for future lookup. + /// + /// # Arguments + /// + /// * `fs` - The filesystem implementation to use. + /// * `path` - The directory path where the space will be created. + /// * `name` - A human-readable name for the space. + /// + /// # Errors + /// + /// Returns an error if: + /// - The path already contains a space. + /// - The directory cannot be created. + /// - The configuration file cannot be written. + async fn init(fs: Self::Fs, path: impl AsRef + Send + Sync, name: impl Into) -> Result; + + /// Load an existing space from the given locator. + /// + /// The locator can specify either a space name (which is resolved + /// from the global configuration) or an explicit filesystem path. + /// + /// # Arguments + /// + /// * `fs` - The filesystem implementation to use. + /// * `locator` - Identifies which space to load. + /// + /// # Errors + /// + /// Returns an error if: + /// - The space cannot be found. + /// - The space configuration is invalid or corrupted. + /// - The filesystem cannot be read. + async fn load(fs: Self::Fs, locator: Locator) -> Result; + + /// Returns the name of the space. + /// + /// The name is a human-readable identifier for the space, set during + /// initialization. It is used for display purposes and can be used + /// to look up the space in the registry. + /// + /// # Examples + /// + /// ```ignore + /// let space = DefaultSpace::init(fs, "./my-space", "personal").await?; + /// assert_eq!(space.name(), "personal"); + /// ``` + fn name(&self) -> &str; + + /// Returns the filesystem path to the space directory. + /// + /// This is the root directory containing the `.flow/` subdirectory + /// and all space content. + /// + /// # Examples + /// + /// ```ignore + /// let space = DefaultSpace::init(fs, "./my-space", "personal").await?; + /// assert_eq!(space.path(), Path::new("./my-space")); + /// ``` + fn path(&self) -> &Path; +} diff --git a/crates/flow/src/main.rs b/crates/flow/src/main.rs index b80392b..6f3b019 100644 --- a/crates/flow/src/main.rs +++ b/crates/flow/src/main.rs @@ -28,10 +28,6 @@ fn version() -> &'static str { #[command(version = version())] #[command(about = "Flow - Note taking for developers")] struct Flow { - /// Space to use (registered name or path), falls back to registered default space if not set. - #[arg(short, long, global = true)] - space: Option, - /// Sub-commands. #[command(subcommand)] command: Option, @@ -63,7 +59,7 @@ async fn main() -> Result<()> { Some(Commands::Gui) => flow_gui::run().await?, #[cfg(feature = "server")] Some(Commands::Serve) => flow_server::run().await?, - Some(Commands::Cli(cmd)) => flow_cli::run(&cmd)?, + Some(Commands::Cli(cmd)) => flow_cli::run(cmd).await?, None => { // TODO: Maybe later we can just launch the UI the user selected in the configuration. diff --git a/deny.toml b/deny.toml index 44b2ee7..c0c6351 100644 --- a/deny.toml +++ b/deny.toml @@ -23,6 +23,7 @@ ignore = [] [licenses] confidence-threshold = 0.8 unused-allowed-license = "allow" +unused-license-exception = "allow" allow = [ "MIT", "Apache-2.0", @@ -36,7 +37,11 @@ allow = [ "Unicode-3.0", "CC0-1.0", "MPL-2.0", + "BSL-1.0", ] +# Workspace crates use AGPL-3.0-or-later. Others can use and modify the code, +# but if they run a modified version as a network service (e.g., SaaS), they +# must make their source code available to users (see AGPL Section 13). exceptions = [ { name = "flow", allow = ["AGPL-3.0-or-later"] }, { name = "flow-app", allow = ["AGPL-3.0-or-later"] }, diff --git a/docs/book.toml b/docs/book.toml index d4a292f..93e50ad 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -17,7 +17,7 @@ default-theme = "navy" preferred-dark-theme = "navy" smart-punctuation = true git-repository-url = "https://github.com/mrbandler/flow" -edit-url-template = "https://github.com/mrbandler/flow/edit/master/docs/{path}" +edit-url-template = "https://github.com/mrbandler/flow/edit/main/docs/{path}" site-url = "/flow/" cname = ""