u
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { defineTool } from 'libllmorbiter';
|
||||
import { getStore } from '../db/store.js';
|
||||
import { isSkillPath } from './skill.js';
|
||||
import { resolveVfsRoot } from './vfs.js';
|
||||
|
||||
@@ -40,25 +39,8 @@ export function createResetVfsTool({ googleAuthId }) {
|
||||
},
|
||||
},
|
||||
invoke: async ({ include_skills = false } = {}) => {
|
||||
const store = await getStore();
|
||||
const keepSkills = include_skills !== true;
|
||||
|
||||
const keptSkills = [];
|
||||
const existing = await store.modelvfs.list(googleAuthId);
|
||||
|
||||
// Phase 1 — clear the database, but only for the names we may delete. Done before
|
||||
// touching disk so a store failure cannot leave the disk wiped while the database
|
||||
// still claims the files exist.
|
||||
for (const file of existing) {
|
||||
if (keepSkills && isSkillPath(file.name)) {
|
||||
keptSkills.push(file.name);
|
||||
continue;
|
||||
}
|
||||
await store.modelvfs.delete(googleAuthId, file.name);
|
||||
}
|
||||
|
||||
// Phase 2 — mirror the same decision onto the on-disk cache. The root comes from
|
||||
// the same lookup the store used above, so the two phases cannot drift apart.
|
||||
const vfsRoot = await resolveVfsRoot(googleAuthId);
|
||||
let deletedCount = 0;
|
||||
if (fs.existsSync(vfsRoot)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { defineTool, ToolError } from 'libllmorbiter';
|
||||
import { getStore } from '../db/store.js';
|
||||
import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js';
|
||||
import { isSkillPath } from './skill.js';
|
||||
|
||||
@@ -97,18 +96,12 @@ export function createRmTool({ googleAuthId }) {
|
||||
const vfsRoot = await prepareVfs({ googleAuthId });
|
||||
const { abs, size } = deleteVfsFile(vfsRoot, clean);
|
||||
|
||||
// Database first. If this throws, the file is still on disk — recoverable, and the row
|
||||
// still exists to describe it. Doing it the other way round would leave a blob on
|
||||
// disk that the next prepareVfs hydration would restore, silently undoing the delete.
|
||||
const store = await getStore();
|
||||
const res = await store.modelvfs.delete(googleAuthId, clean);
|
||||
|
||||
fs.unlinkSync(abs);
|
||||
|
||||
const wasSkill = isSkillPath(clean);
|
||||
const result = {
|
||||
success: true,
|
||||
deleted: res?.deleted !== false,
|
||||
deleted: true,
|
||||
filename: clean,
|
||||
size,
|
||||
message: `Deleted "${clean}" (${size} bytes). This cannot be undone.`,
|
||||
|
||||
@@ -126,7 +126,7 @@ async function vfsRootFor(store, googleAuthId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads default SKILL files into the selected company's VFS (both modelvfs table and disk cache).
|
||||
* Loads default SKILL files into the selected company's on-disk VFS.
|
||||
*
|
||||
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
|
||||
* @returns {Promise<Array<string>>} List of loaded skill file names
|
||||
@@ -140,19 +140,12 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
|
||||
const loaded = [];
|
||||
|
||||
for (const skill of defaultSkills) {
|
||||
try {
|
||||
await store.modelvfs.write(googleAuthId, {
|
||||
name: skill.name,
|
||||
mime: 'text/markdown',
|
||||
content: Buffer.from(skill.content, 'utf8'),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const diskPath = path.resolve(vfsRoot, skill.name);
|
||||
fs.writeFileSync(diskPath, skill.content, 'utf8');
|
||||
if (!fs.existsSync(diskPath)) {
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
fs.writeFileSync(diskPath, skill.content, 'utf8');
|
||||
}
|
||||
loaded.push(skill.name);
|
||||
} catch (err) {
|
||||
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
|
||||
@@ -178,7 +171,7 @@ export async function initCompanyVfs({ googleAuthId, store: passedStore, vfsRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the local directory for this company's VFS exists and is hydrated from modelvfs.
|
||||
* Ensure the local directory for this company's VFS exists and default skills are loaded.
|
||||
*
|
||||
* @param {{ googleAuthId: number }} ctx
|
||||
* @returns {Promise<string>} vfsRoot directory path
|
||||
@@ -189,86 +182,21 @@ export async function prepareVfs({ googleAuthId }) {
|
||||
fs.mkdirSync(vfsRoot, { recursive: true });
|
||||
|
||||
try {
|
||||
const files = await store.modelvfs.list(googleAuthId);
|
||||
const existingNames = new Set(files.map((f) => f.name));
|
||||
const defaultSkills = getDefaultSkills();
|
||||
for (const skill of defaultSkills) {
|
||||
if (!existingNames.has(skill.name)) {
|
||||
try {
|
||||
await store.modelvfs.write(googleAuthId, {
|
||||
name: skill.name,
|
||||
mime: 'text/markdown',
|
||||
content: Buffer.from(skill.content, 'utf8'),
|
||||
});
|
||||
files.push({ name: skill.name });
|
||||
existingNames.add(skill.name);
|
||||
} catch (err) {
|
||||
console.warn(`[vfs] failed to write default skill "${skill.name}" to modelvfs:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
const diskPath = path.resolve(vfsRoot, file.name);
|
||||
if (!diskPath.startsWith(vfsRoot)) continue;
|
||||
if (!fs.existsSync(diskPath)) {
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
const fullRecord = await store.modelvfs.read(googleAuthId, file.name);
|
||||
if (fullRecord?.content) {
|
||||
fs.writeFileSync(diskPath, fullRecord.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
await loadDefaultSkills({ googleAuthId, store, vfsRoot });
|
||||
} catch (err) {
|
||||
console.warn('[vfs] hydration warning:', err.message);
|
||||
console.warn('[vfs] prepare warning:', err.message);
|
||||
}
|
||||
|
||||
return vfsRoot;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
|
||||
* Purely disk-based VFS; no database syncing needed. Kept as no-op for backward compatibility.
|
||||
*
|
||||
* The store re-scopes by the currently selected company on every call, so this only stays
|
||||
* correct while `vfsRoot` was produced by `prepareVfs`/`resolveVfsRoot` in the same turn.
|
||||
*
|
||||
* @param {{ googleAuthId: number, vfsRoot: string }} ctx
|
||||
* @param {{ googleAuthId?: number, vfsRoot?: string }} [ctx]
|
||||
*/
|
||||
export async function syncVfsToDb({ googleAuthId, vfsRoot }) {
|
||||
if (!fs.existsSync(vfsRoot)) return;
|
||||
const store = await getStore();
|
||||
|
||||
function walk(dir, rel = '') {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const list = [];
|
||||
for (const e of entries) {
|
||||
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
||||
const fullPath = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
list.push(...walk(fullPath, relPath));
|
||||
} else if (e.isFile()) {
|
||||
list.push({ relPath, fullPath });
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
const currentFiles = walk(vfsRoot);
|
||||
for (const f of currentFiles) {
|
||||
if (isReadOnlyDatabasePath(f.relPath)) continue;
|
||||
try {
|
||||
const content = fs.readFileSync(f.fullPath);
|
||||
const mime = getMimeType(f.relPath);
|
||||
await store.modelvfs.write(googleAuthId, {
|
||||
name: f.relPath,
|
||||
mime,
|
||||
content,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[vfs] failed to sync file ${f.relPath} to db:`, err.message);
|
||||
}
|
||||
}
|
||||
export async function syncVfsToDb({ googleAuthId, vfsRoot } = {}) {
|
||||
// Purely disk-based VFS; no database syncing.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user