import { defineStore } from 'pinia' import { ref } from 'vue' import { useApi } from '@/composables/useApi' import { useInstancesStore } from '@/stores/instances' export interface FileEntry { name: string path: string is_dir: boolean size: number modified: string } /** * Per-instance file browser store. * All operations target `/api/instances/{id}/...` — jailed to instance root. * Guard: if no current instance, list() sets error and bails out early. */ export const useFilesStore = defineStore('files', () => { const api = useApi() const cwd = ref('') const entries = ref([]) const loading = ref(false) const error = ref(null) /** Join two relative path segments with a single forward slash. */ function joinPath(base: string, name: string): string { if (!base) return name return `${base}/${name}` } function currentId(): string | null { // Retrieve fresh from the store each call — avoids stale closure. return useInstancesStore().currentId } /** List a directory. Sets cwd + entries. Does NOT throw — sets error. */ async function list(path: string): Promise { const id = currentId() if (!id) { error.value = 'No instance — connect the host agent' entries.value = [] return } loading.value = true error.value = null try { const data = await api.get<{ entries: FileEntry[] }>( `/instances/${id}/files?path=${encodeURIComponent(path)}`, ) cwd.value = path entries.value = data.entries ?? [] } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to load directory' } finally { loading.value = false } } /** Read a text file. Returns content string. Throws on error (binary/too big/not found). */ async function readFile(path: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') const data = await api.get<{ content: string }>( `/instances/${id}/file?path=${encodeURIComponent(path)}`, ) return data.content ?? '' } /** Write / overwrite a text file. Throws on error. */ async function writeFile(path: string, content: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.put(`/instances/${id}/file`, { path, content }) } /** Delete a file or directory (recursive). Throws on error. */ async function del(path: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/delete`, { path }) } /** Rename within the same parent. `name` is the bare new filename. Throws on error. */ async function rename(path: string, name: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/rename`, { path, name }) } /** Create a directory (and all missing ancestors). Throws on error. */ async function mkdir(path: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/mkdir`, { path }) } /** Create an empty file. Throws on error. */ async function mkfile(path: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/mkfile`, { path }) } /** Move a file or directory. Both paths are relative to the instance root. Throws on error. */ async function move(path: string, dest: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/move`, { path, dest }) } /** Copy a file or directory. Both paths are relative to the instance root. Throws on error. */ async function copy(path: string, dest: string): Promise { const id = currentId() if (!id) throw new Error('No instance selected') await api.post(`/instances/${id}/files/copy`, { path, dest }) } return { cwd, entries, loading, error, joinPath, list, readFile, writeFile, del, rename, mkdir, mkfile, move, copy, } })