summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-21 17:32:49 +0000
committersoryu <soryu@soryu.co>2026-01-21 17:32:59 +0000
commit9e286c146e29e714b3b209b4d948d75cce179b05 (patch)
treee833c5314cfc089f9cd2c34dff8376cb083cbaad
parent94e5604e770d6589f786ea71e51738e21492f301 (diff)
downloadsoryu-9e286c146e29e714b3b209b4d948d75cce179b05.tar.gz
soryu-9e286c146e29e714b3b209b4d948d75cce179b05.zip
Enforce phaseguard
-rw-r--r--makima/src/bin/makima.rs77
-rw-r--r--makima/src/daemon/api/contract.rs25
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs9
3 files changed, 93 insertions, 18 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 29388e1..67eefc6 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -962,24 +962,44 @@ async fn run_tui_loop(
Ok(result) => {
let contract_name = result.0.get("name")
.and_then(|v| v.as_str())
- .unwrap_or(&name);
- app.status_message = Some(format!("Created contract: {}", contract_name));
+ .unwrap_or(&name)
+ .to_string();
+ let contract_id = result.0.get("id")
+ .and_then(|v| v.as_str())
+ .and_then(|s| uuid::Uuid::parse_str(s).ok());
+
+ // Add repository if provided
+ if let (Some(repo_url), Some(cid)) = (repository_url.as_ref(), contract_id) {
+ if !repo_url.is_empty() {
+ // Extract repo name from URL (e.g., "owner/repo" from GitHub URL)
+ let repo_name = extract_repo_name(repo_url);
+ match client.add_remote_repository(cid, &repo_name, repo_url, true).await {
+ Ok(_) => {
+ app.status_message = Some(format!(
+ "Created contract '{}' with repository",
+ contract_name
+ ));
+ }
+ Err(e) => {
+ app.status_message = Some(format!(
+ "Created contract but failed to add repository: {}",
+ e
+ ));
+ }
+ }
+ } else {
+ app.status_message = Some(format!("Created contract: {}", contract_name));
+ }
+ } else {
+ app.status_message = Some(format!("Created contract: {}", contract_name));
+ }
// Refresh the contracts list
match load_contracts(client).await {
Ok(items) => app.set_items(items),
- Err(e) => app.status_message = Some(format!("Created but refresh failed: {}", e)),
- }
-
- // TODO: If repository_url was provided, add it to the contract
- if let Some(repo_url) = repository_url {
- if !repo_url.is_empty() {
- // We'd need to add a method to add repository to contract
- // For now, just note it in the status
- app.status_message = Some(format!(
- "Created contract: {} (Note: Add repository {} manually)",
- contract_name, repo_url
- ));
+ Err(e) => {
+ let msg = app.status_message.take().unwrap_or_default();
+ app.status_message = Some(format!("{} (refresh failed: {})", msg, e));
}
}
}
@@ -1070,6 +1090,35 @@ async fn run_tui_loop(
Ok(None)
}
+/// Extract a repository name from a URL.
+/// E.g., "https://github.com/owner/repo.git" -> "owner/repo"
+fn extract_repo_name(url: &str) -> String {
+ // Remove .git suffix if present
+ let url = url.trim_end_matches(".git");
+
+ // Try to extract owner/repo from common Git hosting URLs
+ if let Some(path) = url.strip_prefix("https://github.com/")
+ .or_else(|| url.strip_prefix("https://gitlab.com/"))
+ .or_else(|| url.strip_prefix("https://bitbucket.org/"))
+ .or_else(|| url.strip_prefix("git@github.com:"))
+ .or_else(|| url.strip_prefix("git@gitlab.com:"))
+ .or_else(|| url.strip_prefix("git@bitbucket.org:"))
+ {
+ // Return owner/repo
+ return path.to_string();
+ }
+
+ // Fallback: try to get the last path segment
+ if let Some(last_segment) = url.rsplit('/').next() {
+ if !last_segment.is_empty() {
+ return last_segment.to_string();
+ }
+ }
+
+ // Last resort: use the full URL as the name
+ url.to_string()
+}
+
/// Parse an output entry from the API response into an OutputLine
fn parse_output_entry(entry: &serde_json::Value) -> Option<OutputLine> {
let message_type = entry.get("messageType")
diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs
index 50fd64f..12ebe95 100644
--- a/makima/src/daemon/api/contract.rs
+++ b/makima/src/daemon/api/contract.rs
@@ -247,4 +247,29 @@ impl ApiClient {
self.get(&format!("/api/v1/settings/repository-history/suggestions{}", query_string))
.await
}
+
+ /// Add a remote repository to a contract.
+ pub async fn add_remote_repository(
+ &self,
+ contract_id: Uuid,
+ name: &str,
+ repository_url: &str,
+ is_primary: bool,
+ ) -> Result<JsonValue, ApiError> {
+ let req = AddRemoteRepositoryRequest {
+ name: name.to_string(),
+ repository_url: repository_url.to_string(),
+ is_primary,
+ };
+ self.post(&format!("/api/v1/contracts/{}/repositories/remote", contract_id), &req)
+ .await
+ }
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct AddRemoteRepositoryRequest {
+ name: String,
+ repository_url: String,
+ is_primary: bool,
}
diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs
index 6cdbba6..668ea7b 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -1589,8 +1589,9 @@ pub async fn ask_question(
).await;
}
- // If non_blocking mode is enabled, return immediately with the question_id
- if request.non_blocking {
+ // If non_blocking mode or phaseguard is enabled, return immediately with the question_id
+ // Phaseguard questions persist until answered (no timeout) and are displayed by the frontend
+ if request.non_blocking || request.phaseguard {
return (
StatusCode::OK,
Json(AskQuestionResponse {
@@ -1622,8 +1623,8 @@ pub async fn ask_question(
).into_response();
}
- // Check timeout (skip timeout if phaseguard is enabled - wait indefinitely)
- if !request.phaseguard && start.elapsed() >= timeout_duration {
+ // Check timeout
+ if start.elapsed() >= timeout_duration {
// Remove the pending question on timeout
state.remove_pending_question(question_id);