From c94b1760bb5b49e9e5a6ed537865c7e1aafc312f Mon Sep 17 00:00:00 2001 From: Son Seong Jun <166786347+SonAIengine@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:21:11 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=8C=80=ED=99=94=ED=98=95=20REPL=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=97=90=20=EC=A4=84=20=ED=8E=B8=EC=A7=91?= =?UTF-8?q?=EA=B3=BC=200600=20=EA=B8=B0=EB=A1=9D=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20=EB=B6=99=EC=98=80=EB=8B=A4=20(ADR-0071)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 터미널에서는 rustyline 18.0.1(기본 기능 끔, with-file-history만)로 한 줄씩 읽는다. 왼쪽 화살표가 ^[[D로 입력에 섞이던 문제가 사라지고, 위 화살표로 이전 목표를 다시 부른다. - REPL이 쓴 prompt를 PromptTrackingOutput이 기억하고 escape를 뺀 뒤 editor가 다시 그린다 - 입력 중 Ctrl+C는 그 줄만 지우고, 실행 중 취소는 기존 SIGINT 경로 그대로 - 기록은 state root의 repl-history.txt(0600), 목표·명령만 저장하고 승인 응답은 제외 - 파이프·테스트 입력은 기존 stdin reader 유지 tmux 실사용으로 편집·기록·승인 prompt 표시를 확인했고 PendingPrompt 단위 테스트 1개를 더했다. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 47 +++++++ Cargo.toml | 1 + crates/xgen-cli/Cargo.toml | 1 + crates/xgen-cli/src/lib.rs | 9 ++ crates/xgen-cli/src/main.rs | 40 ++++-- crates/xgen-cli/src/repl.rs | 193 +++++++++++++++++++++++++++++ docs/adr/0071-repl-line-editing.md | 37 ++++++ 7 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 docs/adr/0071-repl-line-editing.md diff --git a/Cargo.lock b/Cargo.lock index 8459913..9dd3225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + [[package]] name = "cmov" version = "0.5.4" @@ -688,6 +697,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + [[package]] name = "event-listener" version = "5.4.2" @@ -1876,6 +1891,25 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rustyline" +version = "18.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "clipboard-win", + "libc", + "log", + "memchr", + "nix 0.31.3", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.61.2", +] + [[package]] name = "ryu-js" version = "0.2.2" @@ -2334,6 +2368,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -2846,6 +2892,7 @@ dependencies = [ "process-wrap", "rpassword", "rusqlite", + "rustyline", "serde", "serde_jcs", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index adccfe7..c87da42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ process-wrap = { version = "10.0.0", default-features = false, features = ["std" proptest = { version = "1.11.0", default-features = false, features = ["std"] } rpassword = "7.5.4" rusqlite = { version = "0.40.2", features = ["bundled"] } +rustyline = { version = "18.0.1", default-features = false, features = ["with-file-history"] } serde = { version = "1.0.229", features = ["derive"] } serde_jcs = "0.2.0" serde_json = "1.0.151" diff --git a/crates/xgen-cli/Cargo.toml b/crates/xgen-cli/Cargo.toml index 22873a6..b21e5ce 100644 --- a/crates/xgen-cli/Cargo.toml +++ b/crates/xgen-cli/Cargo.toml @@ -23,6 +23,7 @@ ureq.workspace = true keyring.workspace = true rpassword.workspace = true rusqlite.workspace = true +rustyline.workspace = true serde.workspace = true serde_jcs.workspace = true serde_json.workspace = true diff --git a/crates/xgen-cli/src/lib.rs b/crates/xgen-cli/src/lib.rs index e7eb311..b0037cb 100644 --- a/crates/xgen-cli/src/lib.rs +++ b/crates/xgen-cli/src/lib.rs @@ -31,6 +31,15 @@ pub use usage::{CostEstimate, TokenPrices, UsageReport, UsdPerMillion, inspect_l #[doc(hidden)] pub use environment::compatible_environment; +/// Where the interactive REPL keeps its input history, beside the Runs (ADR-0071). +#[doc(hidden)] +#[must_use] +pub fn repl_history_path() -> Option { + run_layout::discover_state_root() + .ok() + .map(|root| root.join("repl-history.txt")) +} + /// Resolve the catalog directory using the same platform and legacy state roots as Runs. #[doc(hidden)] pub fn tool_catalog_directory() -> Result { diff --git a/crates/xgen-cli/src/main.rs b/crates/xgen-cli/src/main.rs index d8e14fa..9af6423 100644 --- a/crates/xgen-cli/src/main.rs +++ b/crates/xgen-cli/src/main.rs @@ -816,16 +816,36 @@ fn interactive_command(debug: bool, max_model_turns: u32, trust: bool) -> ExitCo eprintln!("XGEN_ERROR code={}", PublicRunError::Configuration.code()); return ExitCode::from(PublicRunError::Configuration.exit_code()); }; - let mut input = repl::InterruptibleInput::stdin(cancellation.clone()); - let mut output = std::io::stdout(); - match repl::run_with_display( - &mut input, - &mut output, - &mut host, - &cancellation, - terminal && !debug, - trust, - ) { + let result = if terminal { + // ADR-0071: arrow-key editing and history while the REPL waits for a line. + let prompt = repl::PendingPrompt::default(); + let mut input = repl::InterruptibleInput::terminal( + cancellation.clone(), + prompt.clone(), + xgen_cli::repl_history_path(), + ); + let mut output = repl::PromptTrackingOutput::new(std::io::stdout(), prompt); + repl::run_with_display( + &mut input, + &mut output, + &mut host, + &cancellation, + !debug, + trust, + ) + } else { + let mut input = repl::InterruptibleInput::stdin(cancellation.clone()); + let mut output = std::io::stdout(); + repl::run_with_display( + &mut input, + &mut output, + &mut host, + &cancellation, + false, + trust, + ) + }; + match result { Ok(()) => ExitCode::SUCCESS, Err(error) if error.kind() == ErrorKind::BrokenPipe => ExitCode::SUCCESS, Err(_) => { diff --git a/crates/xgen-cli/src/repl.rs b/crates/xgen-cli/src/repl.rs index 16a1f08..492f3d9 100644 --- a/crates/xgen-cli/src/repl.rs +++ b/crates/xgen-cli/src/repl.rs @@ -270,6 +270,7 @@ impl Cancellation { enum InputMessage { Line(String), + Interrupted, TooLarge, Unavailable, EndOfInput, @@ -283,6 +284,100 @@ pub(crate) struct InterruptibleInput { buffer: Vec, position: usize, ended: bool, + /// Asks the line editor for one line; `None` reads stdin ahead line by line. + requests: Option>, + requested: bool, +} + +/// Goals can name private paths or hosts, so only the user may read the history file. +fn keep_private(path: &Path) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); + } + #[cfg(not(unix))] + let _ = path; +} + +/// The text written after the last line break: the prompt a line editor must redraw. +#[derive(Clone, Default)] +pub(crate) struct PendingPrompt(Arc>); + +impl PendingPrompt { + const MAX_CHARS: usize = 256; + + fn observe(&self, bytes: &[u8]) { + let Ok(mut pending) = self.0.lock() else { + return; + }; + let tail = match bytes.iter().rposition(|byte| matches!(byte, b'\n' | b'\r')) { + Some(index) => { + pending.clear(); + &bytes[index + 1..] + } + None => bytes, + }; + pending.push_str(&String::from_utf8_lossy(tail)); + if pending.chars().count() > Self::MAX_CHARS { + pending.clear(); + } + } + + fn clear(&self) { + if let Ok(mut pending) = self.0.lock() { + pending.clear(); + } + } + + /// The pending text without terminal escape sequences, which a line editor would miscount. + fn text(&self) -> String { + let pending = self + .0 + .lock() + .map(|pending| pending.clone()) + .unwrap_or_default(); + let mut text = String::with_capacity(pending.len()); + let mut characters = pending.chars(); + while let Some(character) = characters.next() { + if character == '\x1b' { + if characters.next() == Some('[') { + for parameter in characters.by_ref() { + if ('@'..='~').contains(¶meter) { + break; + } + } + } + } else if !character.is_control() { + text.push(character); + } + } + text + } +} + +/// Terminal output that remembers the pending prompt for [`InterruptibleInput::terminal`]. +pub(crate) struct PromptTrackingOutput { + inner: W, + prompt: PendingPrompt, +} + +impl PromptTrackingOutput { + pub(crate) const fn new(inner: W, prompt: PendingPrompt) -> Self { + Self { inner, prompt } + } +} + +impl Write for PromptTrackingOutput { + fn write(&mut self, bytes: &[u8]) -> io::Result { + let written = self.inner.write(bytes)?; + self.prompt.observe(&bytes[..written]); + Ok(written) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } } impl InterruptibleInput { @@ -315,6 +410,75 @@ impl InterruptibleInput { buffer: Vec::new(), position: 0, ended: false, + requests: None, + requested: false, + } + } + + /// A line editor with arrow-key editing and history for a terminal (ADR-0071). + /// + /// The editor reads only when the REPL asks for a line, so while a Run is in flight the + /// terminal stays in its normal mode and Ctrl+C reaches the cooperative handler. While the + /// editor is reading, Ctrl+C cancels the line like the plain reader does. + pub(crate) fn terminal( + cancellation: Cancellation, + prompt: PendingPrompt, + history: Option, + ) -> Self { + let (sender, receiver) = mpsc::sync_channel(1); + let (requests, requested) = mpsc::channel::<()>(); + thread::spawn(move || { + let Ok(mut editor) = rustyline::DefaultEditor::new() else { + let _ = sender.send(InputMessage::Unavailable); + return; + }; + if let Some(history) = &history { + if let Some(parent) = history.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = editor.load_history(history); + } + while requested.recv().is_ok() { + let text = prompt.text(); + let read = editor.readline(&text); + // The editor ended its line on the terminal itself. + prompt.clear(); + let message = match read { + Ok(line) if line.len() > MAX_REPL_LINE_BYTES => InputMessage::TooLarge, + Ok(line) => { + // Only goals and commands are worth recalling, not approval answers. + let entry = text.starts_with("xgen> ") || text.starts_with("...> "); + if entry + && !line.trim().is_empty() + && editor.add_history_entry(line.as_str()).is_ok() + && let Some(history) = &history + { + let _ = editor.append_history(history); + keep_private(history); + } + InputMessage::Line(line) + } + Err(rustyline::error::ReadlineError::Interrupted) => InputMessage::Interrupted, + Err(rustyline::error::ReadlineError::Eof) => InputMessage::EndOfInput, + Err(_) => InputMessage::Unavailable, + }; + let ended = matches!( + message, + InputMessage::EndOfInput | InputMessage::Unavailable + ); + if sender.send(message).is_err() || ended { + return; + } + } + }); + Self { + receiver, + cancellation, + buffer: Vec::new(), + position: 0, + ended: false, + requests: Some(requests), + requested: false, } } @@ -323,12 +487,26 @@ impl InterruptibleInput { if self.cancellation.is_requested() { return Err(io::Error::from(io::ErrorKind::Interrupted)); } + if let Some(requests) = &self.requests + && !self.requested + { + if requests.send(()).is_err() { + self.ended = true; + break; + } + self.requested = true; + } match self.receiver.recv_timeout(Duration::from_millis(25)) { Ok(InputMessage::Line(line)) => { + self.requested = false; self.buffer = line.into_bytes(); self.buffer.push(b'\n'); self.position = 0; } + Ok(InputMessage::Interrupted) => { + self.requested = false; + return Err(io::Error::from(io::ErrorKind::Interrupted)); + } Ok(InputMessage::TooLarge) => { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -2040,6 +2218,21 @@ mod tests { use super::*; + #[test] + fn the_pending_prompt_is_the_unstyled_text_after_the_last_line_break() { + let prompt = PendingPrompt::default(); + let mut output = PromptTrackingOutput::new(Vec::new(), prompt.clone()); + write!(output, "answer\n\x1b[1mxgen>\x1b[0m ").unwrap(); + assert_eq!(prompt.text(), "xgen> "); + write!(output, "\nAllow read? [y/a/N] ").unwrap(); + assert_eq!(prompt.text(), "Allow read? [y/a/N] "); + prompt.clear(); + assert_eq!(prompt.text(), ""); + write!(output, "{}", "x".repeat(PendingPrompt::MAX_CHARS + 1)).unwrap(); + assert_eq!(prompt.text(), ""); + assert_eq!(output.inner.len(), 42 + PendingPrompt::MAX_CHARS + 1); + } + #[test] fn model_rejection_keeps_request_until_explicit_resume_or_clear() { for reason in [ diff --git a/docs/adr/0071-repl-line-editing.md b/docs/adr/0071-repl-line-editing.md new file mode 100644 index 0000000..065a927 --- /dev/null +++ b/docs/adr/0071-repl-line-editing.md @@ -0,0 +1,37 @@ +# ADR-0071: 대화형 REPL 입력에 줄 편집과 기록을 붙인다 + +- 상태: Accepted +- 기준일: 2026-10-27 +- 변경 분류: 범용 REPL 입력. protocol·journal·planning context 변경 없음. + +## 문제 + +대화형 `xgen`은 stdin을 줄 단위로 읽었다. 터미널의 cooked mode가 Backspace만 처리했으므로 왼쪽 화살표는 +`^[[D`로 입력에 섞였다. 위 화살표로 이전 목표를 다시 부를 수도 없었다. Claude Code·Codex는 줄 편집과 입력 기록을 +기본으로 준다. + +## 결정 + +stdin과 stdout이 모두 터미널이면 입력 thread가 `rustyline` 18.0.1 editor로 한 줄씩 읽는다. + +- 의존성은 `default-features = false, features = ["with-file-history"]`다. 사용자 정의 keymap, 완성, 색 강조는 + 넣지 않았다. +- 기존 `InterruptibleInput` 구조는 그대로다. REPL이 다음 줄을 요청할 때만 editor가 읽는다. 모델 호출 중 Ctrl+C는 + 기존 SIGINT 취소 경로가 처리하고, 입력 중 Ctrl+C는 그 줄만 지운다(`Interrupted`). Ctrl+D는 입력 끝이다. +- prompt는 REPL이 이미 stdout에 쓴다. `PromptTrackingOutput`이 마지막 줄바꿈 뒤 텍스트를 기억하고, editor는 그 + 텍스트에서 CSI escape와 제어 문자를 뺀 값을 prompt로 다시 그린다. 256자를 넘으면 prompt가 아니라고 보고 버린다. +- 기록 파일은 Run과 같은 state root의 `repl-history.txt`이고 Unix에서 0600이다. 목표와 명령(`xgen> `, `...> ` + prompt에서 받은 줄)만 저장한다. `[y/a/N]` 승인 응답은 저장하지 않는다. +- 터미널이 아니면(파이프, 테스트) 기존 stdin reader와 plain 출력을 그대로 쓴다. + +## 검증 + +- 단위 테스트: styled prompt에서 escape를 뺀 `xgen> `, 승인 prompt 교체, 256자 초과 폐기. +- tmux 실사용(DeepSeek profile, 비trust): 왼쪽 화살표·Backspace 편집, 입력 중 Ctrl+C, 위 화살표 기록 호출, 승인 + prompt가 자기 줄에 그려지는지, 기록 파일에 목표 1줄만 남고 `a`·`y` 응답은 없는지 확인했다. +- 권한 775인 새 `XGEN_STATE_HOME`에서는 main 바이너리도 `internal_safety_failure`로 멈춘다. 이번 변경과 무관한 + 기존 fail-closed 동작이다. 원인을 알려 주는 메시지는 후속 과제로 남긴다. + +## 남은 일 + +- 전체 화면 TUI(입력 영역 고정, 진행 영역 스크롤)는 별도 결정으로 다룬다. From 560c97d117e2136268bba9768ccc21cb3e9a7b60 Mon Sep 17 00:00:00 2001 From: Son Seong Jun <166786347+SonAIengine@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:34:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Windows=EC=9A=A9=20rustyline=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=98=20BSL-1.0=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=84=A0=EC=8A=A4=EB=A5=BC=20=ED=97=88=EC=9A=A9=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=97=90=20=EB=8D=94=ED=96=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clipboard-win 5.4.3과 error-code 3.4.0이 BSL-1.0이라 Quality 라이선스 검사가 실패했다. BSL-1.0은 OSI 승인, 바이너리 배포 고지 의무가 없는 라이선스라 about.toml에 더하고 THIRD_PARTY_LICENSES.txt를 다시 생성했다(+78줄, rustyline 포함 3개 crate). Co-Authored-By: Claude Opus 5.5 --- THIRD_PARTY_LICENSES.txt | 78 ++++++++++++++++++++++++++++++ about.toml | 1 + docs/adr/0071-repl-line-editing.md | 2 + 3 files changed, 81 insertions(+) diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt index f8293c8..8ea0c1e 100644 --- a/THIRD_PARTY_LICENSES.txt +++ b/THIRD_PARTY_LICENSES.txt @@ -4613,6 +4613,8 @@ Used by: - smallvec 1.15.2 (https://github.com/servo/rust-smallvec) - stable_deref_trait 1.2.1 (https://github.com/storyyeller/stable_deref_trait) - tempfile 3.27.0 (https://github.com/Stebalien/tempfile) +- unicode-segmentation 1.13.3 (https://github.com/unicode-rs/unicode-segmentation) +- unicode-width 0.2.2 (https://github.com/unicode-rs/unicode-width) - url 2.5.8 (https://github.com/servo/rust-url) - uuid 1.26.0 (https://github.com/uuid-rs/uuid) - version_check 0.9.5 (https://github.com/SergioBenitez/version_check) @@ -7350,6 +7352,52 @@ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +================================================================================ +License: Boost Software License 1.0 + +Used by: +- error-code 3.4.0 (https://github.com/DoumanAsh/error-code) + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +================================================================================ +License: Boost Software License 1.0 + +Used by: +- clipboard-win 5.4.3 (https://github.com/DoumanAsh/clipboard-win) + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ================================================================================ License: Community Data License Agreement Permissive 2.0 @@ -8780,6 +8828,36 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +License: MIT License + +Used by: +- rustyline 18.0.1 (https://github.com/kkawakam/rustyline) + +The MIT License (MIT) + +Copyright (c) 2015 Katsu Kawakami & Rustyline authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + ================================================================================ License: MIT License diff --git a/about.toml b/about.toml index 550aac0..c11a43c 100644 --- a/about.toml +++ b/about.toml @@ -4,6 +4,7 @@ accepted = [ "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", + "BSL-1.0", "CDLA-Permissive-2.0", "ISC", "MIT", diff --git a/docs/adr/0071-repl-line-editing.md b/docs/adr/0071-repl-line-editing.md index 065a927..de4803f 100644 --- a/docs/adr/0071-repl-line-editing.md +++ b/docs/adr/0071-repl-line-editing.md @@ -23,6 +23,8 @@ stdin과 stdout이 모두 터미널이면 입력 thread가 `rustyline` 18.0.1 ed - 기록 파일은 Run과 같은 state root의 `repl-history.txt`이고 Unix에서 0600이다. 목표와 명령(`xgen> `, `...> ` prompt에서 받은 줄)만 저장한다. `[y/a/N]` 승인 응답은 저장하지 않는다. - 터미널이 아니면(파이프, 테스트) 기존 stdin reader와 plain 출력을 그대로 쓴다. +- Windows에서 rustyline이 끌어오는 `clipboard-win`·`error-code`가 BSL-1.0이다. BSL-1.0은 바이너리 배포 시 고지 + 의무도 없는 OSI 승인 라이선스라 `about.toml` allow-list에 더하고 `THIRD_PARTY_LICENSES.txt`를 다시 생성했다. ## 검증