diff --git a/app/src/RepositoriesList.tsx b/app/src/RepositoriesList.tsx
index 43942e6..25b5a62 100644
--- a/app/src/RepositoriesList.tsx
+++ b/app/src/RepositoriesList.tsx
@@ -1,7 +1,36 @@
+import { For, Show } from "solid-js";
+import { newXrpcKnottyClient } from "./lib/atproto";
+import { createQuery, queryOptions } from "@tanstack/solid-query";
+import { DidChip } from "./components/DidChip";
+
+async function resolveRepositories() {
+ const rpc = newXrpcKnottyClient();
+ const res = await rpc.get("dev.drawbu.knotty.list");
+ if (!res.ok) return null;
+ return res.data;
+}
+
+const repositoriesQueryOptions = () =>
+ queryOptions({
+ queryKey: ["mirrors", "list"],
+ queryFn: () => resolveRepositories(),
+ });
+
export function RepositoriesList() {
+ const query = createQuery(() => repositoriesQueryOptions());
+
return (
mirrored repositories:
+
loading...}>
+ {(data) => (
+
+ )}
+
);
}
diff --git a/app/src/components/DidChip.tsx b/app/src/components/DidChip.tsx
index 2b3bf3d..eddfdb3 100644
--- a/app/src/components/DidChip.tsx
+++ b/app/src/components/DidChip.tsx
@@ -3,6 +3,7 @@ import { createSignal } from "solid-js";
type Props = {
did: Did;
+ full?: boolean;
class?: string;
};
@@ -27,7 +28,9 @@ export function DidChip(props: Props) {
onClick={copy}
class={`group flex items-center gap-1 font-mono text-xs text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-300 transition-colors ${props.class ?? ""}`}
>
- {shortDid(props.did)}
+
+ {props.full ? props.did : shortDid(props.did)}
+
{}
+
+export interface $params {}
+export interface $output extends v.InferXRPCBodyInput {}
+
+declare module "@atcute/lexicons/ambient" {
+ interface XRPCQueries {
+ "dev.drawbu.knotty.list": mainSchema;
+ }
+}
diff --git a/crates/knotty-knot/src/xrpc/knotty/create.rs b/crates/knotty-knot/src/xrpc/knotty/create.rs
index 458e997..48b79cd 100644
--- a/crates/knotty-knot/src/xrpc/knotty/create.rs
+++ b/crates/knotty-knot/src/xrpc/knotty/create.rs
@@ -8,7 +8,7 @@ use knotty_lexicons::dev_drawbu::knotty::create::{CreateOutput, CreateRequest};
use crate::{error::KnotError, repodid, state::KnotState};
-#[axum::debug_handler(state = KnotState)]
+#[axum::debug_handler]
pub async fn handler(
State(state): State,
ExtractServiceAuth(auth): ExtractServiceAuth,
diff --git a/crates/knotty-knot/src/xrpc/knotty/list.rs b/crates/knotty-knot/src/xrpc/knotty/list.rs
index 1bb2f86..8ba2cd8 100644
--- a/crates/knotty-knot/src/xrpc/knotty/list.rs
+++ b/crates/knotty-knot/src/xrpc/knotty/list.rs
@@ -1,7 +1,37 @@
use crate::{error::KnotError, state::KnotState};
-use axum::extract::State;
+use axum::{Json, extract::State};
+use jacquard::types::did::Did;
+use knotty_lexicons::dev_drawbu::knotty::list::{ListOutput, Repo};
-#[axum::debug_handler(state = KnotState)]
-pub async fn handler(State(_state): State) -> Result<(), KnotError> {
- Err(KnotError::not_implemented())
+#[axum::debug_handler]
+pub async fn handler(
+ State(state): State,
+) -> Result>, KnotError> {
+ let dir = match std::fs::read_dir(state.storage_path) {
+ Ok(dir) => dir,
+ Err(err) => {
+ tracing::info!(error = %err, "storage path is unreachable");
+ return Ok(Json(ListOutput {
+ repos: Vec::new(),
+ extra_data: Default::default(),
+ }));
+ }
+ };
+
+ let repos: Vec<_> = dir
+ .into_iter()
+ .flatten()
+ .filter(|entry| entry.file_type().map_or(false, |ft| ft.is_dir()))
+ .map(|entry| entry.file_name().to_string_lossy().to_string())
+ .filter_map(|entry| Did::new_owned(entry).ok())
+ .map(|repo_did| Repo {
+ repo_did,
+ extra_data: Default::default(),
+ })
+ .collect();
+
+ Ok(Json(ListOutput {
+ repos,
+ extra_data: Default::default(),
+ }))
}
diff --git a/crates/knotty-knot/src/xrpc/knotty/mod.rs b/crates/knotty-knot/src/xrpc/knotty/mod.rs
index e240028..2e6a6b0 100644
--- a/crates/knotty-knot/src/xrpc/knotty/mod.rs
+++ b/crates/knotty-knot/src/xrpc/knotty/mod.rs
@@ -1,6 +1,6 @@
-use axum::{Router, routing::post};
+use axum::Router;
use jacquard_axum::IntoRouter;
-use knotty_lexicons::dev_drawbu::knotty::create::CreateRequest;
+use knotty_lexicons::dev_drawbu::knotty::{create::CreateRequest, list::ListRequest};
use crate::state::KnotState;
@@ -10,5 +10,5 @@ mod list;
pub fn router() -> Router {
Router::new()
.merge(CreateRequest::into_router(create::handler))
- .route("/xrpc/dev.drawbu.knotty.list", post(list::handler))
+ .merge(ListRequest::into_router(list::handler))
}
diff --git a/crates/knotty-lexicons/src/dev_drawbu/knotty.rs b/crates/knotty-lexicons/src/dev_drawbu/knotty.rs
index 5cb8c20..fb83c3b 100644
--- a/crates/knotty-lexicons/src/dev_drawbu/knotty.rs
+++ b/crates/knotty-lexicons/src/dev_drawbu/knotty.rs
@@ -4,3 +4,4 @@
// Any manual changes will be overwritten on the next regeneration.
pub mod create;
+pub mod list;
diff --git a/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs b/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs
new file mode 100644
index 0000000..8f64a8c
--- /dev/null
+++ b/crates/knotty-lexicons/src/dev_drawbu/knotty/list.rs
@@ -0,0 +1,236 @@
+// @generated by jacquard-lexicon. DO NOT EDIT.
+//
+// Lexicon: dev.drawbu.knotty.list
+//
+// This file was automatically generated from Lexicon schemas.
+// Any manual changes will be overwritten on the next regeneration.
+
+#[allow(unused_imports)]
+use alloc::collections::BTreeMap;
+
+#[allow(unused_imports)]
+use core::marker::PhantomData;
+
+#[allow(unused_imports)]
+use jacquard_common::deps::codegen::unicode_segmentation::UnicodeSegmentation;
+use jacquard_common::types::string::Did;
+use jacquard_derive::{IntoStatic, lexicon};
+use jacquard_lexicon::lexicon::LexiconDoc;
+use jacquard_lexicon::schema::LexiconSchema;
+
+use crate::dev_drawbu::knotty::list;
+#[allow(unused_imports)]
+use jacquard_lexicon::validation::{ConstraintError, ValidationPath};
+use serde::{Deserialize, Serialize};
+
+#[lexicon]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)]
+#[serde(rename_all = "camelCase")]
+pub struct ListOutput<'a> {
+ ///List of mirrored repositories.
+ #[serde(borrow)]
+ pub repos: Vec>,
+}
+
+/// A mirrored repository.
+
+#[lexicon]
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic)]
+#[serde(rename_all = "camelCase")]
+pub struct Repo<'a> {
+ ///DID of the mirrored repository.
+ #[serde(borrow)]
+ pub repo_did: Did<'a>,
+}
+
+/// XRPC request marker type.
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, IntoStatic, Copy)]
+pub struct List;
+/// Response type for dev.drawbu.knotty.list
+pub struct ListResponse;
+impl jacquard_common::xrpc::XrpcResp for ListResponse {
+ const NSID: &'static str = "dev.drawbu.knotty.list";
+ const ENCODING: &'static str = "application/json";
+ type Output<'de> = ListOutput<'de>;
+ type Err<'de> = jacquard_common::xrpc::GenericError<'de>;
+}
+
+impl jacquard_common::xrpc::XrpcRequest for List {
+ const NSID: &'static str = "dev.drawbu.knotty.list";
+ const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query;
+ type Response = ListResponse;
+}
+
+/// Endpoint type for dev.drawbu.knotty.list
+pub struct ListRequest;
+impl jacquard_common::xrpc::XrpcEndpoint for ListRequest {
+ const PATH: &'static str = "/xrpc/dev.drawbu.knotty.list";
+ const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query;
+ type Request<'de> = List;
+ type Response = ListResponse;
+}
+
+impl<'a> LexiconSchema for Repo<'a> {
+ fn nsid() -> &'static str {
+ "dev.drawbu.knotty.list"
+ }
+ fn def_name() -> &'static str {
+ "repo"
+ }
+ fn lexicon_doc() -> LexiconDoc<'static> {
+ lexicon_doc_dev_drawbu_knotty_list()
+ }
+ fn validate(&self) -> Result<(), ConstraintError> {
+ Ok(())
+ }
+}
+
+pub mod repo_state {
+
+ pub use crate::builder_types::{IsSet, IsUnset, Set, Unset};
+ #[allow(unused)]
+ use core::marker::PhantomData;
+ mod sealed {
+ pub trait Sealed {}
+ }
+ /// State trait tracking which required fields have been set
+ pub trait State: sealed::Sealed {
+ type RepoDid;
+ }
+ /// Empty state - all required fields are unset
+ pub struct Empty(());
+ impl sealed::Sealed for Empty {}
+ impl State for Empty {
+ type RepoDid = Unset;
+ }
+ ///State transition - sets the `repo_did` field to Set
+ pub struct SetRepoDid(PhantomData S>);
+ impl sealed::Sealed for SetRepoDid {}
+ impl State for SetRepoDid {
+ type RepoDid = Set;
+ }
+ /// Marker types for field names
+ #[allow(non_camel_case_types)]
+ pub mod members {
+ ///Marker type for the `repo_did` field
+ pub struct repo_did(());
+ }
+}
+
+/// Builder for constructing an instance of this type
+pub struct RepoBuilder<'a, S: repo_state::State> {
+ _state: PhantomData S>,
+ _fields: (Option>,),
+ _lifetime: PhantomData<&'a ()>,
+}
+
+impl<'a> Repo<'a> {
+ /// Create a new builder for this type
+ pub fn new() -> RepoBuilder<'a, repo_state::Empty> {
+ RepoBuilder::new()
+ }
+}
+
+impl<'a> RepoBuilder<'a, repo_state::Empty> {
+ /// Create a new builder with all fields unset
+ pub fn new() -> Self {
+ RepoBuilder {
+ _state: PhantomData,
+ _fields: (None,),
+ _lifetime: PhantomData,
+ }
+ }
+}
+
+impl<'a, S> RepoBuilder<'a, S>
+where
+ S: repo_state::State,
+ S::RepoDid: repo_state::IsUnset,
+{
+ /// Set the `repoDid` field (required)
+ pub fn repo_did(
+ mut self,
+ value: impl Into>,
+ ) -> RepoBuilder<'a, repo_state::SetRepoDid> {
+ self._fields.0 = Option::Some(value.into());
+ RepoBuilder {
+ _state: PhantomData,
+ _fields: self._fields,
+ _lifetime: PhantomData,
+ }
+ }
+}
+
+impl<'a, S> RepoBuilder<'a, S>
+where
+ S: repo_state::State,
+ S::RepoDid: repo_state::IsSet,
+{
+ /// Build the final struct
+ pub fn build(self) -> Repo<'a> {
+ Repo {
+ repo_did: self._fields.0.unwrap(),
+ extra_data: Default::default(),
+ }
+ }
+ /// Build the final struct with custom extra_data
+ pub fn build_with_data(
+ self,
+ extra_data: BTreeMap<
+ jacquard_common::deps::smol_str::SmolStr,
+ jacquard_common::types::value::Data<'a>,
+ >,
+ ) -> Repo<'a> {
+ Repo {
+ repo_did: self._fields.0.unwrap(),
+ extra_data: Some(extra_data),
+ }
+ }
+}
+
+fn lexicon_doc_dev_drawbu_knotty_list() -> LexiconDoc<'static> {
+ use alloc::collections::BTreeMap;
+ #[allow(unused_imports)]
+ use jacquard_common::{CowStr, deps::smol_str::SmolStr, types::blob::MimeType};
+ use jacquard_lexicon::lexicon::*;
+ LexiconDoc {
+ lexicon: Lexicon::Lexicon1,
+ id: CowStr::new_static("dev.drawbu.knotty.list"),
+ defs: {
+ let mut map = BTreeMap::new();
+ map.insert(
+ SmolStr::new_static("main"),
+ LexUserType::XrpcQuery(LexXrpcQuery {
+ parameters: None,
+ ..Default::default()
+ }),
+ );
+ map.insert(
+ SmolStr::new_static("repo"),
+ LexUserType::Object(LexObject {
+ description: Some(CowStr::new_static("A mirrored repository.")),
+ required: Some(vec![SmolStr::new_static("repoDid")]),
+ properties: {
+ #[allow(unused_mut)]
+ let mut map = BTreeMap::new();
+ map.insert(
+ SmolStr::new_static("repoDid"),
+ LexObjectProperty::String(LexString {
+ description: Some(CowStr::new_static(
+ "DID of the mirrored repository.",
+ )),
+ format: Some(LexStringFormat::Did),
+ ..Default::default()
+ }),
+ );
+ map
+ },
+ ..Default::default()
+ }),
+ );
+ map
+ },
+ ..Default::default()
+ }
+}
diff --git a/lexicons/dev/drawbu/knotty/create.json b/lexicons/dev/drawbu/knotty/create.json
index a3d4be8..dde73bb 100644
--- a/lexicons/dev/drawbu/knotty/create.json
+++ b/lexicons/dev/drawbu/knotty/create.json
@@ -1,4 +1,5 @@
{
+ "$schema": "https://unpkg.com/@atcute/lexicon-doc/schema/lexicon-doc.schema.json",
"lexicon": 1,
"id": "dev.drawbu.knotty.create",
"defs": {
diff --git a/lexicons/dev/drawbu/knotty/list.json b/lexicons/dev/drawbu/knotty/list.json
new file mode 100644
index 0000000..42bf667
--- /dev/null
+++ b/lexicons/dev/drawbu/knotty/list.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "https://unpkg.com/@atcute/lexicon-doc/schema/lexicon-doc.schema.json",
+ "lexicon": 1,
+ "id": "dev.drawbu.knotty.list",
+ "defs": {
+ "main": {
+ "type": "query",
+ "description": "List all mirrored repositories hosted on this server.",
+ "output": {
+ "encoding": "application/json",
+ "schema": {
+ "type": "object",
+ "required": ["repos"],
+ "properties": {
+ "repos": {
+ "type": "array",
+ "description": "List of mirrored repositories.",
+ "items": {
+ "type": "ref",
+ "ref": "#repo"
+ }
+ }
+ }
+ }
+ }
+ },
+ "repo": {
+ "type": "object",
+ "description": "A mirrored repository.",
+ "required": ["repoDid"],
+ "properties": {
+ "repoDid": {
+ "type": "string",
+ "format": "did",
+ "description": "DID of the mirrored repository."
+ }
+ }
+ }
+ }
+}