Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -16,28 +16,65 @@ function remote(path: string) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function missing(path: string): never { throw Object.assign(new Error(`File does not exist: ${path}`), { code: 'ENOENT' }); }
|
||||
function missing(path: string): never {
|
||||
throw Object.assign(new Error(`File does not exist: ${path}`), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
export interface FileFingerprint { size: number; sha256: string }
|
||||
export interface FileFingerprint {
|
||||
size: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
/** One scoped agent scan; null preserves compatibility with older agents and local files. */
|
||||
export async function nativeFileManifest(root: string, directories: string[], ignoreNames: string[], managedPaths: string[]): Promise<Map<string, FileFingerprint> | null> {
|
||||
export async function nativeFileManifest(
|
||||
root: string,
|
||||
directories: string[],
|
||||
ignoreNames: string[],
|
||||
managedPaths: string[]
|
||||
): Promise<Map<string, FileFingerprint> | null> {
|
||||
const target = remote(root);
|
||||
if (!target || target.path !== '.' || !getHost(target.instance.host_id)?.inventory.capabilities?.includes('files.manifest')) return null;
|
||||
const roots = [...new Set(directories.map(directory => relative(root, directory).split(sep).join('/')))];
|
||||
if (
|
||||
!target ||
|
||||
target.path !== '.' ||
|
||||
!getHost(target.instance.host_id)?.inventory.capabilities?.includes('files.manifest')
|
||||
)
|
||||
return null;
|
||||
const roots = [
|
||||
...new Set(directories.map((directory) => relative(root, directory).split(sep).join('/')))
|
||||
];
|
||||
// Preserve legacy configurations that scan the entire instance root.
|
||||
if (roots.some(directory => !directory)) return null;
|
||||
const paths = [...new Set(managedPaths.map(path => relative(root, path).split(sep).join('/')))];
|
||||
const result = await nativeRequest<{ files: Array<FileFingerprint & { path: string }> }>(target.instance, 'files.manifest', {
|
||||
roots, paths, ignoreNames: ignoreNames.filter(name => !paths.some(path => path.split('/').includes(name)))
|
||||
});
|
||||
if (!result || !Array.isArray(result.files) || result.files.length > 10_000) throw new Error('Agent returned an invalid file manifest.');
|
||||
if (roots.some((directory) => !directory)) return null;
|
||||
const paths = [
|
||||
...new Set(managedPaths.map((path) => relative(root, path).split(sep).join('/')))
|
||||
];
|
||||
const result = await nativeRequest<{ files: Array<FileFingerprint & { path: string }> }>(
|
||||
target.instance,
|
||||
'files.manifest',
|
||||
{
|
||||
roots,
|
||||
paths,
|
||||
ignoreNames: ignoreNames.filter(
|
||||
(name) => !paths.some((path) => path.split('/').includes(name))
|
||||
)
|
||||
}
|
||||
);
|
||||
if (!result || !Array.isArray(result.files) || result.files.length > 10_000)
|
||||
throw new Error('Agent returned an invalid file manifest.');
|
||||
const manifest = new Map<string, FileFingerprint>();
|
||||
for (const entry of result.files) {
|
||||
if (!entry || typeof entry.path !== 'string' || !entry.path || entry.path.includes('\\') || entry.path.includes('\0')
|
||||
|| posix.normalize(entry.path) !== entry.path || entry.path.split('/').some(part => part === '..' || part === '.')
|
||||
|| !roots.some(directory => entry.path.startsWith(`${directory}/`))
|
||||
|| !Number.isSafeInteger(entry.size) || entry.size < 0 || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry.path !== 'string' ||
|
||||
!entry.path ||
|
||||
entry.path.includes('\\') ||
|
||||
entry.path.includes('\0') ||
|
||||
posix.normalize(entry.path) !== entry.path ||
|
||||
entry.path.split('/').some((part) => part === '..' || part === '.') ||
|
||||
!roots.some((directory) => entry.path.startsWith(`${directory}/`)) ||
|
||||
!Number.isSafeInteger(entry.size) ||
|
||||
entry.size < 0 ||
|
||||
!/^[a-f0-9]{64}$/.test(entry.sha256)
|
||||
) {
|
||||
throw new Error('Agent returned an invalid file manifest entry.');
|
||||
}
|
||||
const path = join(root, entry.path);
|
||||
@@ -50,44 +87,85 @@ export async function nativeFileManifest(root: string, directories: string[], ig
|
||||
export async function readFile(path: string): Promise<Buffer> {
|
||||
const target = remote(path);
|
||||
if (!target) return fs.readFile(path);
|
||||
const result = await nativeRequest<{ contentSha256?: string; contentBase64?: string | null } | null>(target.instance, 'files.read', { path: target.path });
|
||||
const result = await nativeRequest<{
|
||||
contentSha256?: string;
|
||||
contentBase64?: string | null;
|
||||
} | null>(target.instance, 'files.read', { path: target.path });
|
||||
// Stored receipts reference shared bytes; read them directly without a base64 round trip.
|
||||
if (typeof result?.contentSha256 === 'string') return getFileBlob(result.contentSha256);
|
||||
if (result?.contentBase64 === null) return missing(path);
|
||||
if (typeof result?.contentBase64 === 'string') return Buffer.from(result.contentBase64, 'base64');
|
||||
if (typeof result?.contentBase64 === 'string')
|
||||
return Buffer.from(result.contentBase64, 'base64');
|
||||
throw new Error(`Agent returned an invalid response for files.read: ${target.path}`);
|
||||
}
|
||||
|
||||
export async function exists(path: string): Promise<boolean> {
|
||||
try { await stat(path); return true; }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; throw error; }
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function stat(path: string): Promise<{ size: number; mtime: Date; isDirectory(): boolean; isFile(): boolean }> {
|
||||
export async function stat(
|
||||
path: string
|
||||
): Promise<{ size: number; mtime: Date; isDirectory(): boolean; isFile(): boolean }> {
|
||||
const target = remote(path);
|
||||
if (!target) return fs.stat(path);
|
||||
const result = await nativeRequest<{ exists?: boolean; size: number; mtime: string | number; isDirectory: boolean; isFile: boolean } | null>(target.instance, 'files.stat', { path: target.path });
|
||||
const result = await nativeRequest<{
|
||||
exists?: boolean;
|
||||
size: number;
|
||||
mtime: string | number;
|
||||
isDirectory: boolean;
|
||||
isFile: boolean;
|
||||
} | null>(target.instance, 'files.stat', { path: target.path });
|
||||
if (result === null || result.exists === false) return missing(path);
|
||||
return { size: result.size, mtime: new Date(typeof result.mtime === 'number' ? result.mtime * 1000 : result.mtime), isDirectory: () => result.isDirectory, isFile: () => result.isFile };
|
||||
return {
|
||||
size: result.size,
|
||||
mtime: new Date(typeof result.mtime === 'number' ? result.mtime * 1000 : result.mtime),
|
||||
isDirectory: () => result.isDirectory,
|
||||
isFile: () => result.isFile
|
||||
};
|
||||
}
|
||||
|
||||
export async function readdir(path: string, options: { withFileTypes: true }): Promise<{ name: string; isDirectory(): boolean; isFile(): boolean }[]> {
|
||||
export async function readdir(
|
||||
path: string,
|
||||
options: { withFileTypes: true }
|
||||
): Promise<{ name: string; isDirectory(): boolean; isFile(): boolean }[]> {
|
||||
const target = remote(path);
|
||||
if (!target) return fs.readdir(path, options);
|
||||
const result = await nativeRequest<{ entries: { name: string; isDirectory: boolean; isFile?: boolean }[] }>(target.instance, 'files.list', { path: target.path });
|
||||
return result.entries.map(entry => ({ name: entry.name, isDirectory: () => entry.isDirectory, isFile: () => entry.isFile ?? !entry.isDirectory }));
|
||||
const result = await nativeRequest<{
|
||||
entries: { name: string; isDirectory: boolean; isFile?: boolean }[];
|
||||
}>(target.instance, 'files.list', { path: target.path });
|
||||
return result.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isDirectory: () => entry.isDirectory,
|
||||
isFile: () => entry.isFile ?? !entry.isDirectory
|
||||
}));
|
||||
}
|
||||
|
||||
export async function nativeTail(path: string, lines: number) {
|
||||
const target = remote(path);
|
||||
if (!target) return undefined;
|
||||
return nativeRequest<import('../fleet/logs.js').TailResult | null>(target.instance, 'logs.read', { path: target.path, lines });
|
||||
return nativeRequest<import('../fleet/logs.js').TailResult | null>(
|
||||
target.instance,
|
||||
'logs.read',
|
||||
{ path: target.path, lines }
|
||||
);
|
||||
}
|
||||
|
||||
export async function nativeLogChunk(path: string, cursor?: import('../fleet/logs.js').LogCursor) {
|
||||
const target = remote(path);
|
||||
if (!target) return undefined;
|
||||
const incremental = getHost(target.instance.host_id)?.inventory.capabilities?.includes('logs.read-offset');
|
||||
return nativeRequest<import('../fleet/logs.js').LogChunk | import('../fleet/logs.js').TailResult | null>(target.instance, 'logs.read',
|
||||
{ path: target.path, ...(incremental ? { mode: 'incremental', cursor } : { lines: 2000 }) });
|
||||
const incremental = getHost(target.instance.host_id)?.inventory.capabilities?.includes(
|
||||
'logs.read-offset'
|
||||
);
|
||||
return nativeRequest<
|
||||
import('../fleet/logs.js').LogChunk | import('../fleet/logs.js').TailResult | null
|
||||
>(target.instance, 'logs.read', {
|
||||
path: target.path,
|
||||
...(incremental ? { mode: 'incremental', cursor } : { lines: 2000 })
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user