Skip to content

Commit c501c0c

Browse files
authored
Merge pull request #22 from lorem-dev/develop
release: 0.2.2
2 parents 23a62b0 + 1d172fa commit c501c0c

33 files changed

Lines changed: 1714 additions & 79 deletions

‎.github/workflows/backmerge.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
open-backmerge-pr:
2222
runs-on: ubuntu-latest
2323
steps:
24-
- uses: actions/checkout@v4
24+
- uses: actions/checkout@v5
2525
with:
2626
fetch-depth: 0
2727

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
lfs: true
5555

5656
- name: Install uv
57-
uses: astral-sh/setup-uv@v5
57+
uses: astral-sh/setup-uv@v7
5858

5959
- name: Configure git identity for mike
6060
run: |

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ jobs:
387387
# dir (linux/windows) and the cross-target dir (macOS --target).
388388
# -----------------------------------------------------------------------
389389
- name: Upload artifacts
390-
uses: actions/upload-artifact@v4
390+
uses: actions/upload-artifact@v6
391391
with:
392392
name: skillkeeper-${{ github.ref_name }}-${{ matrix.target }}
393393
path: |
@@ -436,7 +436,7 @@ jobs:
436436
run: node scripts/release-notes.mjs
437437

438438
- name: Download all artifacts
439-
uses: actions/download-artifact@v4
439+
uses: actions/download-artifact@v7
440440
with:
441441
path: dist-all
442442

@@ -505,7 +505,7 @@ jobs:
505505
run: node scripts/release-downloads.mjs "$GITHUB_REF_NAME" dist
506506

507507
- name: Create GitHub release and attach artifacts
508-
uses: softprops/action-gh-release@v2
508+
uses: softprops/action-gh-release@v3
509509
with:
510510
files: dist/*
511511
body_path: RELEASE_NOTES.md

‎CHANGES.md‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,83 @@
22

33
## Development
44

5+
## Version 0.2.2
6+
7+
### Added
8+
9+
- A documentation section on setting up an ssh-agent, with per-platform
10+
instructions and a note on pinentry. When the terminal is raised for a
11+
passphrase and no agent is running, the app now says so once per session and
12+
links to it -- otherwise being asked on every single operation reads as the
13+
app being broken rather than as a machine that needs setting up. The Windows
14+
instructions call out that Git for Windows uses its own bundled `ssh`, which
15+
ignores the Windows agent service until `core.sshCommand` points at the system
16+
client.
17+
- A right-click menu in the terminal with Copy, Paste and Select all. Copying
18+
was previously reachable only through a keyboard shortcut that differs per
19+
platform (and where a bare Ctrl+C sends an interrupt instead), so getting text
20+
out was easy to miss. Select all also covers the case where a full-screen
21+
program has taken over the mouse and dragging cannot select.
22+
23+
### Fixed
24+
25+
- The terminal is raised for every prompt that needs an answer, not just the
26+
first one of a session, and the caret lands in it so the answer can be typed
27+
straight away instead of after a click nobody expects. Credential prompts over
28+
HTTPS (`Username for ...`, `Password for ...`) now count as prompts too; only
29+
an SSH passphrase and a host-key confirmation did before.
30+
- The update check skips a repository that was never cloned instead of fetching
31+
in a directory that does not exist. Such a repository has no local commit to
32+
compare against, so the check could only ever answer "no update" -- but it
33+
answered it by running git first, which reported a fatal error into the
34+
terminal on every startup, once per uncloned repository.
35+
- Windows: an SSH key passphrase can finally be entered, so cloning a repository
36+
that needs one works. Git used to run in a pseudo-terminal of its own there,
37+
and the passphrase prompt never surfaced in it -- git simply waited forever,
38+
printing nothing. It now runs in the interactive terminal the user is already
39+
looking at, exactly as it does on macOS and Linux: the prompt appears in the
40+
terminal and can be answered. Because Windows shells take no prompt hook, the
41+
command reports its exit code by printing a sentinel that is stripped from the
42+
output before it is shown. An ssh-agent is still used when one holds the key,
43+
in which case nothing is asked at all.
44+
- A git command that blocks on a prompt no longer wedges every later one. Git
45+
runs in the terminal through its own pseudo-terminal, and the app waited for
46+
it to exit without any bound -- so a clone stuck on an SSH key passphrase (a
47+
prompt that does not reach that pseudo-terminal on Windows) held the git queue
48+
for the rest of the session, and each following clone, sync or update check
49+
blocked silently before it could print anything. The wait is now bounded by
50+
SILENCE rather than runtime -- a large clone reports progress throughout, so
51+
it is never affected -- and a command that produces nothing at all while still
52+
running is stopped, with a message naming the likely cause.
53+
- Windows: the first repository operation that ran git in the terminal never
54+
finished, and every later one silently did nothing. After git exited, the app
55+
waited for the output reader to reach end-of-file while still holding the
56+
command's pseudo-terminal open -- and Windows keeps a copy of the write handle
57+
alive for exactly that long, so the end-of-file could never arrive. The call
58+
never returned, its slot in the git queue was never released, and each
59+
following clone, sync or update check blocked before it could print anything:
60+
the terminal accepted typing but showed no git, an update check sat unfinished,
61+
and adding a repository looked like nothing happened. The pseudo-terminal is
62+
now released before the wait, and the wait itself is bounded.
63+
- A terminal that cannot start now says so instead of staying blank. The
64+
renderer used to issue the start as a floating promise, so a failed shell
65+
spawn produced no error anywhere -- and because repository git only runs
66+
through the terminal while a session is live, the same dead session silently
67+
reverted clone/sync to a headless git that prints nothing. The failure is now
68+
shown in the terminal view, logged as a warning that explains git is still
69+
running without visible output, and reported by a new `terminal_status`
70+
command. Starting is retried up to three times before it is reported.
71+
- A pseudo-terminal backend that panics rather than returning an error -- Windows
72+
hosts older than Windows 10 1809, where `CreatePseudoConsole` is not exported --
73+
no longer unwinds through the command's worker task into an opaque join error.
74+
The panic is caught and its message kept, so the terminal is unavailable with a
75+
readable reason instead of failing anonymously.
76+
- Git launch failures on the standalone-process path (Windows and unintegrated
77+
shells) are printed to the terminal instead of being swallowed into a bare
78+
exit code, and a `git` that cannot be spawned names the likely cause: it is not
79+
on the PATH this application inherited. Shell spawn errors likewise name the
80+
shell and working directory they tried.
81+
582
## Version 0.2.1
683

784
### Added

‎Cargo.lock‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010

1111
[workspace.package]
1212
edition = "2021"
13-
version = "0.2.1"
13+
version = "0.2.2"
1414
license = "Apache-2.0"
1515

1616
[workspace.dependencies]

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@skillkeeper/desktop",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"private": true,
55
"description": "Install and manage skills and hooks for AI coding agents",
66
"author": {

‎apps/desktop/src-tauri/src/app/ssh_agent.rs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,32 @@ fn has_inherited_agent() -> bool {
6464
.unwrap_or(false)
6565
}
6666

67+
/// The named pipe the Windows OpenSSH agent service listens on. Windows sets no
68+
/// `SSH_AUTH_SOCK`, so its presence is the only way to tell the service is up.
69+
#[cfg(windows)]
70+
const WINDOWS_AGENT_PIPE: &str = r"\\.\pipe\openssh-ssh-agent";
71+
72+
/// Whether an ssh-agent is available to hold a key for this session.
73+
///
74+
/// Consumed by the `ssh_agent_available` command: with no agent, every SSH
75+
/// operation has to ask for the key passphrase, so the app points the user at
76+
/// the setup documentation instead of leaving the repeated prompts unexplained.
77+
///
78+
/// This reports only that an agent EXISTS, not that it holds the right key --
79+
/// listing keys would mean running `ssh-add -l` on a path where a hung agent
80+
/// could block, and a false "available" only costs the hint, not correctness.
81+
pub fn is_available() -> bool {
82+
if has_inherited_agent() {
83+
return true;
84+
}
85+
#[cfg(windows)]
86+
{
87+
return std::path::Path::new(WINDOWS_AGENT_PIPE).exists();
88+
}
89+
#[cfg(not(windows))]
90+
false
91+
}
92+
6793
/// Load the default ssh keys once, best-effort, without ever blocking on a
6894
/// passphrase. No TTY (not a shell) plus `SSH_ASKPASS_REQUIRE=never` / empty
6995
/// `SSH_ASKPASS` / `DISPLAY` means an encrypted key absent from the keychain

‎apps/desktop/src-tauri/src/commands/repositories.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,14 @@ pub fn has_update(ctx: &AppContext, id: String) -> bool {
507507
Ok(Some(repo)) => repo,
508508
_ => return false,
509509
};
510+
// A repository that was never cloned (its clone failed, or the directory was
511+
// deleted) has no local commit to compare against, so there is nothing this
512+
// can report. Checking first also keeps the update sweep from running
513+
// `git -C <missing dir> fetch` -- which prints a `fatal:` into the terminal
514+
// on every startup, once per such repository. Matches `list_branches`.
515+
if !ctx.fs.exists(&repo.local_path).unwrap_or(false) {
516+
return false;
517+
}
510518
// Fetch in the terminal (visible, ssh-capable) like a pull; the rev-parse
511519
// comparisons below stay on the silent port.
512520
if fetch_op(ctx, &repo.local_path).is_err() {
@@ -887,6 +895,29 @@ mod tests {
887895
assert!(has_update(&app.ctx, repo.id.clone()));
888896
}
889897

898+
/// A tracked repository that was never cloned must not be fetched: there is
899+
/// nothing to compare against, and running git in a directory that does not
900+
/// exist only reports a fatal error into the terminal on every sweep.
901+
#[test]
902+
fn has_update_is_false_without_touching_git_when_the_clone_is_missing() {
903+
if !git_available() {
904+
eprintln!("skipping: git not available");
905+
return;
906+
}
907+
let app = TempAppData::new();
908+
let src = SourceRepo::new();
909+
let repo = add_repo(&app, &src);
910+
// Added but never cloned, so the recorded local path does not exist.
911+
assert!(!Path::new(&repo.local_path).exists());
912+
assert!(!has_update(&app.ctx, repo.id.clone()));
913+
914+
// And once it IS cloned, the same call reports normally again.
915+
assert!(clone(&app.ctx, repo.id.clone()).ok);
916+
assert!(!has_update(&app.ctx, repo.id.clone()));
917+
src.advance("extra.txt");
918+
assert!(has_update(&app.ctx, repo.id));
919+
}
920+
890921
#[test]
891922
fn update_edits_name_and_rederives_remote_fields() {
892923
if !git_available() {

‎apps/desktop/src-tauri/src/commands/terminal.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,39 @@
1515
//! setup. In-shell git (`run_git`) is exposed on the manager for Wave 4 to call
1616
//! and is not yet surfaced as a command.
1717
18+
use serde::Serialize;
1819
use tauri::State;
1920

2021
use std::sync::Arc;
2122

2223
use super::blocking;
2324
use crate::state::AppContext;
2425

26+
/// Whether a live shell session exists, and why not when it does not.
27+
///
28+
/// The repository commands run git through the PTY only while a session is
29+
/// live, falling back to the silent headless port otherwise. The renderer reads
30+
/// this to explain that fallback instead of leaving the user with a terminal
31+
/// that shows nothing during a clone.
32+
#[derive(Debug, Serialize)]
33+
#[serde(rename_all = "camelCase")]
34+
pub struct TerminalStatus {
35+
/// True once the shell has started (git output streams to the view).
36+
pub started: bool,
37+
/// The last shell/git launch failure, when there is one.
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
pub error: Option<String>,
40+
}
41+
42+
/// `terminal:status` -- is the shell live, and if not, why not.
43+
#[tauri::command]
44+
pub fn terminal_status(state: State<'_, Arc<AppContext>>) -> TerminalStatus {
45+
TerminalStatus {
46+
started: state.terminal.is_started(),
47+
error: state.terminal.last_error(),
48+
}
49+
}
50+
2551
/// `terminal:start` -- (re)ensure the shell is running at `cols` x `rows` and
2652
/// return its retained scrollback for the renderer to replay.
2753
#[tauri::command]
@@ -57,6 +83,16 @@ pub fn terminal_clear_buffer(state: State<'_, Arc<AppContext>>) -> Result<(), St
5783
Ok(())
5884
}
5985

86+
/// `ssh:agentAvailable` -- whether an ssh-agent exists to hold a key.
87+
///
88+
/// Without one, every SSH operation has to ask for the passphrase again, so the
89+
/// renderer uses this to explain the repeated prompts and point at the setup
90+
/// documentation rather than leaving them unexplained.
91+
#[tauri::command]
92+
pub fn ssh_agent_available() -> bool {
93+
crate::app::ssh_agent::is_available()
94+
}
95+
6096
/// `terminal:runSshAdd` -- type `ssh-add` into the interactive shell so its key
6197
/// passphrase prompt appears in the terminal view (macOS adds
6298
/// `--apple-use-keychain`).

0 commit comments

Comments
 (0)