**Motivations:** - Seed AnythingLLM workspace from cloned repo using gitignore-style filters **Root causes:** - N/A **Correctifs:** - N/A **Evolutions:** - Template 4nkaiignore.default; server copies after clone; extension uploads via POST /api/v1/document/upload - New commands /workspace-sync; settings initialSync*; dependency ignore **Pages affectées:** - extensions/anythingllm-workspaces/* - services/repos-devtools-server/* - docs/features/initial-rag-sync-4nkaiignore.md
147 lines
3.9 KiB
TypeScript
147 lines
3.9 KiB
TypeScript
import * as fs from "node:fs/promises";
|
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
import * as path from "node:path";
|
|
import { runGit } from "./gitSpawn.js";
|
|
import { readJsonBody } from "./httpUtil.js";
|
|
import {
|
|
getCodeRoot,
|
|
isGitRepo,
|
|
repoDirForName,
|
|
repoNameFromGitUrl,
|
|
} from "./paths.js";
|
|
import { copyDefault4nkaiignoreIfMissing } from "./write4nkaiignore.js";
|
|
|
|
const json = (res: ServerResponse, status: number, body: unknown): void => {
|
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
res.end(JSON.stringify(body));
|
|
};
|
|
|
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
|
const readBranch = (body: unknown, fallback: string): string => {
|
|
if (!isRecord(body)) {
|
|
return fallback;
|
|
}
|
|
const b = body.branch;
|
|
return typeof b === "string" && b.trim().length > 0 ? b.trim() : fallback;
|
|
};
|
|
|
|
const readUrl = (body: unknown): string => {
|
|
if (!isRecord(body)) {
|
|
throw new Error("Expected JSON object with url");
|
|
}
|
|
const u = body.url;
|
|
if (typeof u !== "string" || u.trim().length === 0) {
|
|
throw new Error("Missing or invalid url");
|
|
}
|
|
return u.trim();
|
|
};
|
|
|
|
const readName = (body: unknown): string => {
|
|
if (!isRecord(body)) {
|
|
throw new Error("Expected JSON object with name");
|
|
}
|
|
const n = body.name;
|
|
if (typeof n !== "string" || n.trim().length === 0) {
|
|
throw new Error("Missing or invalid name");
|
|
}
|
|
return n.trim();
|
|
};
|
|
|
|
export const handleReposClone = async (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
): Promise<void> => {
|
|
const codeRoot = getCodeRoot();
|
|
const body = await readJsonBody(req);
|
|
const url = readUrl(body);
|
|
const branch = readBranch(body, "test");
|
|
const name = repoNameFromGitUrl(url);
|
|
const dest = path.join(codeRoot, name);
|
|
try {
|
|
await fs.access(dest);
|
|
json(res, 409, {
|
|
error: "Target directory already exists",
|
|
name,
|
|
path: dest,
|
|
});
|
|
return;
|
|
} catch {
|
|
/* absent */
|
|
}
|
|
const r = await runGit(
|
|
["clone", "-b", branch, "--single-branch", url, dest],
|
|
codeRoot,
|
|
);
|
|
if (r.code !== 0) {
|
|
json(res, 500, {
|
|
error: "git clone failed",
|
|
code: r.code,
|
|
stderr: r.stderr,
|
|
stdout: r.stdout,
|
|
});
|
|
return;
|
|
}
|
|
let fourNkAiIgnoreTemplateWrote = false;
|
|
try {
|
|
const c = await copyDefault4nkaiignoreIfMissing(dest);
|
|
fourNkAiIgnoreTemplateWrote = c.wrote;
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
json(res, 500, {
|
|
error: "clone ok but failed to write default .4nkaiignore template",
|
|
detail: msg,
|
|
name,
|
|
path: dest,
|
|
});
|
|
return;
|
|
}
|
|
json(res, 200, {
|
|
ok: true,
|
|
name,
|
|
path: dest,
|
|
branch,
|
|
url,
|
|
fourNkAiIgnoreTemplateWrote,
|
|
});
|
|
};
|
|
|
|
export const handleReposList = async (res: ServerResponse): Promise<void> => {
|
|
const codeRoot = getCodeRoot();
|
|
const entries = await fs.readdir(codeRoot, { withFileTypes: true });
|
|
const repos: { name: string; path: string }[] = [];
|
|
for (const ent of entries) {
|
|
if (!ent.isDirectory()) {
|
|
continue;
|
|
}
|
|
const full = path.join(codeRoot, ent.name);
|
|
if (await isGitRepo(full)) {
|
|
repos.push({ name: ent.name, path: full });
|
|
}
|
|
}
|
|
repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
json(res, 200, { repos, codeRoot });
|
|
};
|
|
|
|
export const handleReposLoad = async (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
): Promise<void> => {
|
|
const codeRoot = getCodeRoot();
|
|
const body = await readJsonBody(req);
|
|
const name = readName(body);
|
|
const dest = repoDirForName(codeRoot, name);
|
|
try {
|
|
await fs.access(dest);
|
|
} catch {
|
|
json(res, 404, { error: "Repository directory not found", name, path: dest });
|
|
return;
|
|
}
|
|
if (!(await isGitRepo(dest))) {
|
|
json(res, 400, { error: "Directory is not a git repository", name, path: dest });
|
|
return;
|
|
}
|
|
json(res, 200, { ok: true, name, path: dest });
|
|
};
|