Merge branch 'main' into worktree-agent-a0d19db53ce9b29d8
15 files+85−230/15 viewed
| 1 | + | * @syntaqx |
| 1096 | 1096 | ||
| 1097 | 1097 | [[package]] | |
| 1098 | 1098 | name = "g1t-runner" | |
| 1099 | − | version = "0.1.2" | |
| 1099 | + | version = "0.1.3" | |
| 1100 | 1100 | dependencies = [ | |
| 1101 | 1101 | "anyhow", | |
| 1102 | 1102 | "base64 0.22.1", |
| 132 | 132 | matches!(url.scheme(), "http" | "https") && origin(url) == origin(&server(app)) | |
| 133 | 133 | } | |
| 134 | 134 | ||
| 135 | + | /// Where the bundled pages are served under `tauri dev`: with no `devUrl` | |
| 136 | + | /// in tauri.conf.json, the Tauri CLI serves `launcher/` itself, from | |
| 137 | + | /// localhost, and the window starts there. Unset in a release build, which | |
| 138 | + | /// serves them from inside the app. | |
| 139 | + | static DEV_SERVER: std::sync::OnceLock<Option<Url>> = std::sync::OnceLock::new(); | |
| 140 | + | ||
| 141 | + | /// Notes the dev server, once, as the app starts. | |
| 142 | + | pub fn note_dev_server(app: &AppHandle) { | |
| 143 | + | let dev = if cfg!(debug_assertions) { app.config().build.dev_url.clone() } else { None }; | |
| 144 | + | let _ = DEV_SERVER.set(dev); | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | pub fn dev_server() -> Option<&'static Url> { | |
| 148 | + | DEV_SERVER.get().and_then(Option::as_ref) | |
| 149 | + | } | |
| 150 | + | ||
| 135 | 151 | /// Whether a page is the launcher, bundled with the app. | |
| 136 | 152 | pub fn is_launcher(url: &Url) -> bool { | |
| 137 | − | url.scheme() == "tauri" || (matches!(url.scheme(), "http" | "https") && url.host_str() == Some("tauri.localhost")) | |
| 153 | + | url.scheme() == "tauri" | |
| 154 | + | || (matches!(url.scheme(), "http" | "https") && url.host_str() == Some("tauri.localhost")) | |
| 155 | + | || dev_server().is_some_and(|dev| origin(dev) == origin(url)) | |
| 138 | 156 | } | |
| 139 | 157 | ||
| 140 | 158 | /// What the server's pages may call: the bridge's commands (bridge.rs). |
| 26 | 26 | use tauri::{RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent}; | |
| 27 | 27 | use tauri_plugin_window_state::{StateFlags, WindowExt}; | |
| 28 | 28 | ||
| 29 | + | /// What the window keeps between launches: its size, place, and whether it | |
| 30 | + | /// was maximized or full screen; not its frame. | |
| 31 | + | const KEPT: StateFlags = StateFlags::all().difference(StateFlags::DECORATIONS); | |
| 32 | + | ||
| 29 | 33 | /// The one window's label. | |
| 30 | 34 | pub const WINDOW: &str = "main"; | |
| 31 | 35 | ||
| ⋯ | |||
| 40 | 44 | .plugin(tauri_plugin_opener::init()) | |
| 41 | 45 | .plugin(tauri_plugin_updater::Builder::new().build()) | |
| 42 | 46 | // The window's size and place come back in setup, without the plugin | |
| 43 | − | // also showing it: at sign-in g1t may start in the tray. | |
| 44 | − | .plugin(tauri_plugin_window_state::Builder::new().skip_initial_state(WINDOW).with_denylist(&[settings::SETTINGS]).build()) | |
| 47 | + | // also showing it: at sign-in g1t may start in the tray. Whether it | |
| 48 | + | // has the system's frame is the title bar setting's (frame.rs), never | |
| 49 | + | // what an earlier run left: kept, it would put the frame back round | |
| 50 | + | // g1t's own title bar. | |
| 51 | + | .plugin( | |
| 52 | + | tauri_plugin_window_state::Builder::new() | |
| 53 | + | .with_state_flags(KEPT) | |
| 54 | + | .skip_initial_state(WINDOW) | |
| 55 | + | .with_denylist(&[settings::SETTINGS]) | |
| 56 | + | .build(), | |
| 57 | + | ) | |
| 45 | 58 | // Opening at sign-in, which settings turn on; the argument tells that start from someone's. | |
| 46 | 59 | .plugin(tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec![settings::AT_LOGIN]))) | |
| 47 | 60 | .manage(config::State::default()) | |
| ⋯ | |||
| 66 | 79 | .on_menu_event(|app, event| menu::handle(app, event.id().as_ref())) | |
| 67 | 80 | .setup(|app| { | |
| 68 | 81 | let handle = app.handle().clone(); | |
| 82 | + | config::note_dev_server(&handle); | |
| 69 | 83 | config::load(&handle); | |
| 70 | 84 | config::allow_site(&handle)?; | |
| 71 | 85 | let hidden = settings::starts_hidden(&handle); | |
| ⋯ | |||
| 92 | 106 | let builder = builder.menu(menu::build(&handle)?); | |
| 93 | 107 | let window = frame::style(builder, custom).build()?; | |
| 94 | 108 | // Opened at sign-in, g1t waits in the tray; otherwise the window comes up where it was. | |
| 95 | − | let _ = window.restore_state(StateFlags::all() - StateFlags::VISIBLE); | |
| 109 | + | let _ = window.restore_state(KEPT - StateFlags::VISIBLE); | |
| 96 | 110 | #[cfg(not(target_os = "macos"))] | |
| 97 | 111 | frame::menu_bar(&window, custom); | |
| 98 | 112 | if !hidden { | |
| 96 | 96 | /// The address of the launcher page bundled with the app. | |
| 97 | 97 | pub fn launcher_url(query: &str) -> url::Url { | |
| 98 | 98 | let base = if cfg!(windows) { "http://tauri.localhost/" } else { "tauri://localhost/" }; | |
| 99 | − | let mut url = url::Url::parse(base).expect("the launcher's address is a URL"); | |
| 99 | + | let mut url = config::dev_server().cloned().unwrap_or_else(|| url::Url::parse(base).expect("the launcher's address is a URL")); | |
| 100 | 100 | url.set_path("/index.html"); | |
| 101 | 101 | if !query.is_empty() { | |
| 102 | 102 | url.set_query(Some(query)); |
| 1 | + | /** | |
| 2 | + | * When a Customize page reads its data again: for another page, the other | |
| 3 | + | * view, or a posted form (lib/customize.ts `needsReload`). A route export | |
| 4 | + | * the browser runs, so it lives apart from the server-only helpers. | |
| 5 | + | */ | |
| 6 | + | import type { ShouldRevalidateFunctionArgs } from "react-router"; | |
| 7 | + | ||
| 8 | + | import { needsReload } from "./customize"; | |
| 9 | + | ||
| 10 | + | export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs): boolean { | |
| 11 | + | return needsReload(currentUrl, nextUrl, formMethod) ? defaultShouldRevalidate : false; | |
| 12 | + | } |
| 4 | 4 | * listing tools again, where it reaches, removing it), and what follows | |
| 5 | 5 | * adding one: the browser goes to sign in, or to the connector's page. | |
| 6 | 6 | */ | |
| 7 | − | import { type ShouldRevalidateFunctionArgs, redirect } from "react-router"; | |
| 7 | + | import { redirect } from "react-router"; | |
| 8 | 8 | ||
| 9 | 9 | import type { ConnectorInput, User, WorkspaceConnector } from "@g1t/contracts"; | |
| 10 | 10 | ||
| 11 | 11 | import { addresses } from "./addresses.server"; | |
| 12 | − | import { connectorCallbackPath, connectorPagePath, needsReload, splitTarget } from "./customize"; | |
| 12 | + | import { connectorCallbackPath, connectorPagePath, splitTarget } from "./customize"; | |
| 13 | 13 | import { workspaceAgents } from "./services.server"; | |
| 14 | 14 | import type { ActionResult } from "../components/agents/dialogs"; | |
| 15 | 15 | import { answer } from "../components/agents/actions.server"; | |
| ⋯ | |||
| 17 | 17 | /** Where a connector's sign-in comes back to: this g1t's own page. */ | |
| 18 | 18 | export function callbackUri(slug: string): string { | |
| 19 | 19 | return `${addresses().site}${connectorCallbackPath(slug)}`; | |
| 20 | − | } | |
| 21 | − | ||
| 22 | − | /** A Customize page reads its data again only for another page, the other view, or a posted form (lib/customize.ts). */ | |
| 23 | − | export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs): boolean { | |
| 24 | − | return needsReload(currentUrl, nextUrl, formMethod) ? defaultShouldRevalidate : false; | |
| 25 | 20 | } | |
| 26 | 21 | ||
| 27 | 22 | /** The browser leaves for the connector's sign-in page, or lands on the connector's own page with what happened. */ | |
| 36 | 36 | import { skillLibrary, workspaceAgents } from "../../../lib/services.server"; | |
| 37 | 37 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 38 | 38 | ||
| 39 | − | export { shouldRevalidate } from "../../../lib/customize.server"; | |
| 39 | + | export { shouldRevalidate } from "../../../lib/customize-revalidate"; | |
| 40 | 40 | ||
| 41 | 41 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 42 | 42 | return page(args, { title: `Connectors · Customize · ${params.owner} · g1t` }); |
| 34 | 34 | import { workspaceAgents } from "../../../lib/services.server"; | |
| 35 | 35 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 36 | 36 | ||
| 37 | − | export { shouldRevalidate } from "../../../lib/customize.server"; | |
| 37 | + | export { shouldRevalidate } from "../../../lib/customize-revalidate"; | |
| 38 | 38 | ||
| 39 | 39 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 40 | 40 | return page(args, { title: `Customize · ${params.owner} · g1t` }); |
| 25 | 25 | import { workspaceAgents } from "../../../lib/services.server"; | |
| 26 | 26 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 27 | 27 | ||
| 28 | − | export { shouldRevalidate } from "../../../lib/customize.server"; | |
| 28 | + | export { shouldRevalidate } from "../../../lib/customize-revalidate"; | |
| 29 | 29 | ||
| 30 | 30 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 31 | 31 | return page(args, { title: `Marketplaces · Customize · ${params.owner} · g1t` }); |
| 29 | 29 | import { workspaceAgents } from "../../../lib/services.server"; | |
| 30 | 30 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 31 | 31 | ||
| 32 | − | export { shouldRevalidate } from "../../../lib/customize.server"; | |
| 32 | + | export { shouldRevalidate } from "../../../lib/customize-revalidate"; | |
| 33 | 33 | ||
| 34 | 34 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 35 | 35 | return page(args, { title: `Plugins · Customize · ${params.owner} · g1t` }); |
| 30 | 30 | import { skillLibrary, workspaceAgents } from "../../../lib/services.server"; | |
| 31 | 31 | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 32 | 32 | ||
| 33 | − | export { shouldRevalidate } from "../../../lib/customize.server"; | |
| 33 | + | export { shouldRevalidate } from "../../../lib/customize-revalidate"; | |
| 34 | 34 | ||
| 35 | 35 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 36 | 36 | return page(args, { title: `Skills · Customize · ${params.owner} · g1t` }); |
| 1 | 1 | [package] | |
| 2 | 2 | name = "g1t-runner" | |
| 3 | − | version = "0.1.2" | |
| 3 | + | version = "0.1.3" | |
| 4 | 4 | edition.workspace = true | |
| 5 | 5 | license.workspace = true | |
| 6 | 6 | description = "The program inside a g1t sandbox: runs an agent, checks, a catch-up merge, a review or a GitHub Actions job, and reports back. Also the self-hosted runner, g1t-runner register/run, on your own machines." |
| 315 | 315 | let fetched = self.fetch_to(artifact, &file).and_then(|format| { | |
| 316 | 316 | if format == "tgz" { | |
| 317 | 317 | std::fs::create_dir_all(into).map_err(|e| e.to_string())?; | |
| 318 | − | let script = format!("tar -xzf {} -C {}", quote(&file.display().to_string()), quote(&into.display().to_string())); | |
| 318 | + | let script = format!("tar{} -xzf {} -C {}", tar_local(), quote(&file.display().to_string()), quote(&into.display().to_string())); | |
| 319 | 319 | if self.shell(&script) { Ok(()) } else { Err("it could not be unpacked".into()) } | |
| 320 | 320 | } else { | |
| 321 | 321 | zip::extract(&file, into).map(|_| ()).map_err(|e| format!("it could not be unpacked: {e}")) | |
| ⋯ | |||
| 558 | 558 | let lookup_only = with.get("lookup-only").is_some_and(|v| v == "true"); | |
| 559 | 559 | let size = std::fs::metadata(&archive).map(|m| m.len()).unwrap_or(0); | |
| 560 | 560 | // tar finds out from the archive whether it is zstd or gzip. | |
| 561 | − | if !lookup_only && !self.shell(&format!("tar -xPf {}", quote(&archive.display().to_string()))) { | |
| 561 | + | if !lookup_only && !self.shell(&format!("tar{} -xPf {}", tar_local(), quote(&archive.display().to_string()))) { | |
| 562 | 562 | self.log.line("##[warning]The cache was found but could not be unpacked."); | |
| 563 | 563 | } | |
| 564 | 564 | let _ = std::fs::remove_file(&archive); | |
| ⋯ | |||
| 896 | 896 | for f in \"${{found[@]}}\"; do if [ -e \"$f\" ]; then files+=(\"$f\"); fi; done; \ | |
| 897 | 897 | if [ ${{#files[@]}} -eq 0 ]; then exit 3; fi; \ | |
| 898 | 898 | if command -v zstd >/dev/null; then compress='zstd -T0 -3'; else compress=gzip; fi; \ | |
| 899 | − | tar -cPf {} -I \"$compress\" {} -- \"${{files[@]}}\"", | |
| 899 | + | tar{} -cPf {} -I \"$compress\" {} -- \"${{files[@]}}\"", | |
| 900 | 900 | includes.join(" "), | |
| 901 | + | tar_local(), | |
| 901 | 902 | quote(archive), | |
| 902 | 903 | excludes.join(" ") | |
| 903 | 904 | ) | |
| 904 | 905 | } | |
| 905 | 906 | ||
| 907 | + | /// GNU tar on Windows reads `C:...` as a host and a file; `--force-local` | |
| 908 | + | /// makes every path a local one. Elsewhere the flag is left off, since | |
| 909 | + | /// bsdtar (macOS) doesn't know it. | |
| 910 | + | pub(crate) fn tar_local() -> &'static str { | |
| 911 | + | if cfg!(windows) { | |
| 912 | + | " --force-local" | |
| 913 | + | } else { | |
| 914 | + | "" | |
| 915 | + | } | |
| 916 | + | } | |
| 917 | + | ||
| 906 | 918 | fn urlencode(text: &str) -> String { | |
| 907 | 919 | let mut out = String::new(); | |
| 908 | 920 | for byte in text.bytes() { | |
| ⋯ | |||
| 940 | 952 | } | |
| 941 | 953 | ||
| 942 | 954 | #[test] | |
| 955 | + | fn packing_keeps_windows_paths_local() { | |
| 956 | + | let script = pack_script(&["C:/w/target".into()], "C:/t/c.tar"); | |
| 957 | + | assert_eq!(script.contains("--force-local"), cfg!(windows), "{script}"); | |
| 958 | + | } | |
| 959 | + | ||
| 960 | + | #[test] | |
| 943 | 961 | fn packing_expands_globs_and_leaves_exclusions_out() { | |
| 944 | 962 | let script = pack_script(&["/w/target/*/release".into(), "!/w/target/**/incremental".into()], "/t/c.tar"); | |
| 945 | 963 | assert!(script.contains("found=( /w/target/*/release )"), "{script}"); | |
| 300 | 300 | let _ = std::fs::create_dir_all(&dir); | |
| 301 | 301 | let mut command = Command::new("tar"); | |
| 302 | 302 | command.arg("-xzf").arg(&archive).arg("-C").arg(&dir).arg("--strip-components=1"); | |
| 303 | + | if cfg!(windows) { | |
| 304 | + | // GNU tar on Windows reads `C:...` as a host and a file. | |
| 305 | + | command.arg("--force-local"); | |
| 306 | + | } | |
| 303 | 307 | let mut commands = Commands::default(); | |
| 304 | 308 | matches!(process::run(command, Duration::from_secs(300).min(self.remaining_time()), &mut self.log, &mut commands), Ok(Ended::Exited(0))) | |
| 305 | 309 | } | |
| ⋯ | |||
| 334 | 338 | } | |
| 335 | 339 | let _ = std::fs::create_dir_all(&dir); | |
| 336 | 340 | let url = format!("https://codeload.github.com/{owner}/{repo}/tar.gz/{}", sha.unwrap_or(git_ref)); | |
| 337 | − | let script = format!("set -o pipefail; curl -fsSL --retry 3 '{url}' | tar -xz -C '{}' --strip-components=1", dir.display()); | |
| 341 | + | let script = format!("set -o pipefail; curl -fsSL --retry 3 '{url}' | tar{} -xz -C '{}' --strip-components=1", super::blobs::tar_local(), dir.display()); | |
| 338 | 342 | let mut command = Command::new("bash"); | |
| 339 | 343 | command.args(["-c", &script]); | |
| 340 | 344 | let mut commands = Commands::default(); | |