app: Add-program flow with server-side filesystem picker
Register an existing repo as a program from the /programs page — the web equivalent of `castle program add`. An inline form with an editable, autocompleting path bar browses the *server's* filesystem (a browser's own file dialog only sees the client machine) and also accepts a git URL. - core: extract the adopt logic (target parsing, stack/command detection, ProgramSpec build) into castle_core.adopt so the CLI and API share it; the CLI's add.py now delegates to it. - castle-api: GET /fs/browse (directory listing, per-entry stat errors skipped so one unreadable child can't 403 the dir) and POST /programs/adopt. - app: AddProgramForm + Add-program button on the Programs page; useBrowse / useAdoptProgram hooks. Also tidy ProgramCard: deployments render below the description, and the deployment name label is hidden when it matches the program title.
This commit is contained in:
@@ -385,6 +385,56 @@ export function useProgramSync() {
|
||||
})
|
||||
}
|
||||
|
||||
// Server-side directory browser for the "Add program" flow. Programs live on the
|
||||
// server's filesystem, so the picker browses the server's dirs (the browser's own
|
||||
// file dialog only sees the client machine). `path` null => the repos dir.
|
||||
export interface BrowseEntry {
|
||||
name: string
|
||||
path: string
|
||||
is_program: boolean
|
||||
is_git: boolean
|
||||
}
|
||||
export interface BrowseResult {
|
||||
path: string
|
||||
parent: string | null
|
||||
repos_dir: string
|
||||
entries: BrowseEntry[]
|
||||
}
|
||||
export function useBrowse(path: string | null, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: ["browse", path ?? "@repos"],
|
||||
queryFn: () =>
|
||||
apiClient.get<BrowseResult>(
|
||||
`/fs/browse${path ? `?path=${encodeURIComponent(path)}` : ""}`,
|
||||
),
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
export interface AdoptResult {
|
||||
ok: boolean
|
||||
program: string
|
||||
source: string
|
||||
stack: string | null
|
||||
repo: string | null
|
||||
commands: string[]
|
||||
is_git_url: boolean
|
||||
}
|
||||
// Adopt an existing repo as a program (the web `castle program add`). Refreshes
|
||||
// the catalog + the derived graph/repo views.
|
||||
export function useAdoptProgram() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (body: { target: string; name?: string; description?: string }) =>
|
||||
apiClient.post<AdoptResult>("/programs/adopt", body),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["programs"] })
|
||||
qc.invalidateQueries({ queryKey: ["graph"] })
|
||||
qc.invalidateQueries({ queryKey: ["repos"] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRepos() {
|
||||
return useQuery({
|
||||
queryKey: ["repos"],
|
||||
|
||||
Reference in New Issue
Block a user