import { useMemo, useState } from "react"; import { Effect, Exit } from "effect"; import { Button } from "@executor-js/react/components/button"; import { CardStack, CardStackContent, CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { Input } from "@executor-js/react/components/input"; import { FormErrorAlert, useSlugAlreadyExists } from "@executor-js/react/lib/integration-add"; import { APPS_INTEGRATION_SLUG, createCustomToolSourceEffect, formatSyncErrors, parseGitSourceUrl, slugifyCustomToolsAppName, suggestCustomToolsAppName, syncCustomToolSourceEffect, validateCustomToolsAppSlug, validateGitSourceUrl, type AppSourceKind, } from "./custom-tools-client"; export default function AddCustomToolsSource(props: { readonly onComplete: (slug?: string) => void; readonly onCancel: () => void; readonly initialUrl?: string; readonly initialNamespace?: string; readonly sourceKinds?: readonly AppSourceKind[]; }) { const sourceKinds = props.sourceKinds ?? ["git"]; const allowLocalDirectory = sourceKinds.includes("local-directory"); const initialKind = props.initialUrl && props.initialUrl.startsWith("/") && allowLocalDirectory ? "local-directory" : "git"; const [kind, setKind] = useState(initialKind); const [url, setUrl] = useState(props.initialUrl ?? ""); const [path, setPath] = useState( initialKind === "local-directory" ? (props.initialUrl ?? "") : "", ); const [ref, setRef] = useState(""); const [name, setName] = useState( props.initialNamespace ? slugifyCustomToolsAppName(props.initialNamespace) : "", ); const [nameTouched, setNameTouched] = useState(props.initialNamespace !== undefined); const [token, setToken] = useState(""); const [fieldError, setFieldError] = useState(null); const [nameError, setNameError] = useState(null); const [syncing, setSyncing] = useState(false); const [syncError, setSyncError] = useState(null); const sourceValue = kind === "git" ? url : path; const effectiveName = useMemo( () => (nameTouched ? name : slugifyCustomToolsAppName(suggestCustomToolsAppName(sourceValue))), [name, nameTouched, sourceValue], ); const slug = effectiveName; const slugAlreadyExists = useSlugAlreadyExists(slug); const submit = async () => { const nextFieldError = kind === "git" ? validateGitSourceUrl(url) : path.trim().startsWith("/") ? null : "Enter an absolute directory path."; const nextNameError = validateCustomToolsAppSlug(slug); setFieldError(nextFieldError); setNameError(nextNameError); setSyncError(null); if (nextFieldError || nextNameError) return; if (slugAlreadyExists) { setSyncError(`An integration named "${slug}" already exists. Choose another source name.`); return; } setSyncing(true); const parsedGitUrl = kind === "git" ? parseGitSourceUrl(url) : null; const createExit = await Effect.runPromiseExit( createCustomToolSourceEffect( kind === "git" ? { kind: "git", slug, app: slug, url: parsedGitUrl?.ok ? parsedGitUrl.url : url.trim(), ...(ref.trim() ? { ref: ref.trim() } : {}), ...(token.trim() ? { token: token.trim() } : {}), } : { kind: "local-directory", slug, app: slug, path: path.trim() }, ), ); if (Exit.isFailure(createExit)) { setSyncError("Failed to create custom tools source."); setSyncing(false); return; } const syncExit = await Effect.runPromiseExit( syncCustomToolSourceEffect(createExit.value.source.slug), ); if (Exit.isFailure(syncExit)) { setSyncError("Failed to sync custom tools source."); setSyncing(false); return; } const result = syncExit.value; if (result.status === "failed") { setSyncError(formatSyncErrors(result).join("\n") || "Sync failed."); setSyncing(false); return; } setSyncing(false); props.onComplete(APPS_INTEGRATION_SLUG); }; return (

Add custom tools

Sync tools from a Git repository, then publish them to the Apps catalog.

{allowLocalDirectory && (
)} {kind === "git" ? ( <>
{ setUrl((event.target as HTMLInputElement).value); setFieldError(null); setSyncError(null); }} onBlur={() => setFieldError(validateGitSourceUrl(url))} placeholder="https://github.com/acme/tools.git" className="font-mono text-sm" aria-invalid={fieldError ? true : undefined} /> {fieldError &&

{fieldError}

}
setRef((event.target as HTMLInputElement).value)} placeholder="main" className="font-mono text-sm" disabled={syncing} /> setToken((event.target as HTMLInputElement).value)} autoComplete="off" className="font-mono text-sm" disabled={syncing} /> ) : (
{ setPath((event.target as HTMLInputElement).value); setFieldError(null); setSyncError(null); }} placeholder="/srv/executor-tools" className="font-mono text-sm" aria-invalid={fieldError ? true : undefined} /> {fieldError &&

{fieldError}

}
)}
{ setNameTouched(true); setName(slugifyCustomToolsAppName((event.target as HTMLInputElement).value)); setNameError(null); setSyncError(null); }} placeholder="custom-tools" className="text-sm" aria-invalid={nameError ? true : undefined} /> {nameError &&

{nameError}

} {slugAlreadyExists && !syncing && !nameError && (

An integration named "{slug}" already exists.

)}
{syncError && }
); }