Add repos-devtools-server and AnythingLLM dev tools panel (0.2.0)
**Motivations:** - Clone or load repos under /home/ncantu/code with AnythingLLM workspace ensure/create from the editor **Root causes:** - N/A (new capability) **Correctifs:** - N/A **Evolutions:** - services/repos-devtools-server: POST /repos-clone, GET /repos-list, POST /repos-load (Bearer REPOS_DEVTOOLS_TOKEN) - Extension: Webview panel, slash commands, workspaceEnsure + POST /api/v1/workspace/new - Docs: feature note and index links **Pages affectées:** - services/repos-devtools-server/* - extensions/anythingllm-workspaces/* - docs/README.md - docs/features/repos-devtools-server-and-dev-panel.md - docs/features/anythingllm-vscode-extension.md
This commit is contained in:
parent
564b9d5576
commit
597f18f758
@ -14,6 +14,8 @@ Operational, architectural, and UX-design notes for the local-AI IDE initiative
|
|||||||
| [../deploy/nginx/README-ia-enso.md](../deploy/nginx/README-ia-enso.md) | Proxy HTTPS `ia.enso.4nkweb.com` → Ollama / AnythingLLM (Bearer, script SSH, dépannage) |
|
| [../deploy/nginx/README-ia-enso.md](../deploy/nginx/README-ia-enso.md) | Proxy HTTPS `ia.enso.4nkweb.com` → Ollama / AnythingLLM (Bearer, script SSH, dépannage) |
|
||||||
| [../extensions/anythingllm-workspaces/README.md](../extensions/anythingllm-workspaces/README.md) | Extension VS Code / Cursor : lister les workspaces AnythingLLM (API) et ouvrir l’UI |
|
| [../extensions/anythingllm-workspaces/README.md](../extensions/anythingllm-workspaces/README.md) | Extension VS Code / Cursor : lister les workspaces AnythingLLM (API) et ouvrir l’UI |
|
||||||
| [features/anythingllm-vscode-extension.md](./features/anythingllm-vscode-extension.md) | Fiche évolution : extension AnythingLLM, impacts, modalités |
|
| [features/anythingllm-vscode-extension.md](./features/anythingllm-vscode-extension.md) | Fiche évolution : extension AnythingLLM, impacts, modalités |
|
||||||
|
| [features/repos-devtools-server-and-dev-panel.md](./features/repos-devtools-server-and-dev-panel.md) | API locale repos + panneau dev tools (clone, workspace AnythingLLM) |
|
||||||
|
| [../services/repos-devtools-server/README.md](../services/repos-devtools-server/README.md) | Serveur HTTP local : clone/list/load sous `REPOS_DEVTOOLS_ROOT` |
|
||||||
| [fixKnowledge/anythingllm-extension-403-api-key.md](./fixKnowledge/anythingllm-extension-403-api-key.md) | 403 API AnythingLLM : clé nginx Ollama vs clé UI API Keys |
|
| [fixKnowledge/anythingllm-extension-403-api-key.md](./fixKnowledge/anythingllm-extension-403-api-key.md) | 403 API AnythingLLM : clé nginx Ollama vs clé UI API Keys |
|
||||||
| [features/ia-enso-nginx-proxy-ollama-anythingllm.md](./features/ia-enso-nginx-proxy-ollama-anythingllm.md) | Fiche évolution : objectifs, impacts, modalités du reverse proxy ia.enso |
|
| [features/ia-enso-nginx-proxy-ollama-anythingllm.md](./features/ia-enso-nginx-proxy-ollama-anythingllm.md) | Fiche évolution : objectifs, impacts, modalités du reverse proxy ia.enso |
|
||||||
| [anythingllm-workspaces.md](./anythingllm-workspaces.md) | One AnythingLLM workspace per project; sync pipeline |
|
| [anythingllm-workspaces.md](./anythingllm-workspaces.md) | One AnythingLLM workspace per project; sync pipeline |
|
||||||
|
|||||||
@ -15,6 +15,7 @@ Fournir un point d’entrée minimal dans l’éditeur pour lister les **workspa
|
|||||||
|
|
||||||
- `package.json`, `tsconfig.json`, sources TypeScript (`src/extension.ts`, `src/anythingllmClient.ts`, `src/types.ts`).
|
- `package.json`, `tsconfig.json`, sources TypeScript (`src/extension.ts`, `src/anythingllmClient.ts`, `src/types.ts`).
|
||||||
- `README.md` de l’extension : prérequis, configuration, commandes, lien vers `deploy/nginx/README-ia-enso.md`.
|
- `README.md` de l’extension : prérequis, configuration, commandes, lien vers `deploy/nginx/README-ia-enso.md`.
|
||||||
|
- Évolutions ultérieures (v0.2.0) : panneau dev tools, client `repos-devtools-server`, `POST /api/v1/workspace/new` — voir [repos-devtools-server-and-dev-panel.md](./repos-devtools-server-and-dev-panel.md).
|
||||||
|
|
||||||
## Modalités de déploiement
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
|||||||
34
docs/features/repos-devtools-server-and-dev-panel.md
Normal file
34
docs/features/repos-devtools-server-and-dev-panel.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# repos-devtools-server + panneau « Dev tools » (extension AnythingLLM)
|
||||||
|
|
||||||
|
**Author:** 4NK
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Sur l’hôte qui porte les clones (ex. `192.168.1.164`, racine `/home/ncantu/code`) :
|
||||||
|
|
||||||
|
- exposer une **API HTTP locale** (git clone branche `test`, liste des dépôts, résolution de chemin) ;
|
||||||
|
- depuis l’extension **AnythingLLM Workspaces**, fournir un **panneau Webview** pour saisir des commandes texte et afficher la réponse ;
|
||||||
|
- enchaîner avec l’**API développeur AnythingLLM** pour **vérifier / créer** un workspace dont le nom (ou slug) correspond au dépôt.
|
||||||
|
|
||||||
|
## Impacts
|
||||||
|
|
||||||
|
- Nouveau service : `services/repos-devtools-server/` (Node 20+, écoute `127.0.0.1`, Bearer obligatoire via `REPOS_DEVTOOLS_TOKEN`).
|
||||||
|
- Extension version **0.2.0** : réglages `anythingllm.reposApiBaseUrl`, `anythingllm.reposApiToken`, commande **AnythingLLM: Dev tools panel**, fichiers `media/devTools.js`, logique `workspaceEnsure`, `POST /api/v1/workspace/new`.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
- Serveur : `POST /repos-clone`, `GET /repos-list`, `POST /repos-load`.
|
||||||
|
- Extension : parseur de lignes, client HTTP repos, `ensureWorkspaceForRepoName`, panneau Webview.
|
||||||
|
|
||||||
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
1. Sur la machine des clones : définir `REPOS_DEVTOOLS_TOKEN`, optionnellement `REPOS_DEVTOOLS_ROOT`, `npm run build && npm start` (voir `services/repos-devtools-server/README.md`).
|
||||||
|
2. Dans Cursor / VS Code (même hôte ou tunnel vers `:37140`) : renseigner `anythingllm.reposApiBaseUrl` et `anythingllm.reposApiToken`.
|
||||||
|
3. Recompiler / réinstaller l’extension (`.vsix` ou workspace dev).
|
||||||
|
|
||||||
|
## Modalités d’analyse
|
||||||
|
|
||||||
|
- Erreur **401** sur l’API repos : token extension ≠ `REPOS_DEVTOOLS_TOKEN`.
|
||||||
|
- Erreur **403** AnythingLLM : clé API application (pas secret nginx Ollama).
|
||||||
|
- **409** clone : répertoire cible déjà présent sous `REPOS_DEVTOOLS_ROOT`.
|
||||||
|
- **git clone** échoue si la branche `test` n’existe pas sur le remote (comportement git nominal).
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# AnythingLLM Workspaces (VS Code / Cursor extension)
|
# AnythingLLM Workspaces (VS Code / Cursor extension)
|
||||||
|
|
||||||
Minimal extension to call the **AnythingLLM developer API** and open a workspace in the browser.
|
Extension for the **AnythingLLM developer API**, optional **local repos API** (`repos-devtools-server`), and a **dev tools panel** for slash-style commands.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -9,21 +9,44 @@ Minimal extension to call the **AnythingLLM developer API** and open a workspace
|
|||||||
|
|
||||||
**Do not** use the **nginx Bearer secret** for `/ollama/` (see `deploy/nginx/README-ia-enso.md`). That value is only for the Ollama reverse proxy. AnythingLLM validates API keys against its **own** database; a wrong secret yields `403` with `{"error":"No valid api key found."}`.
|
**Do not** use the **nginx Bearer secret** for `/ollama/` (see `deploy/nginx/README-ia-enso.md`). That value is only for the Ollama reverse proxy. AnythingLLM validates API keys against its **own** database; a wrong secret yields `403` with `{"error":"No valid api key found."}`.
|
||||||
|
|
||||||
|
For **clone / repos-list / repos-load**, run **`services/repos-devtools-server`** on the host that owns the clone root (e.g. `192.168.1.164` with `REPOS_DEVTOOLS_ROOT=/home/ncantu/code`). The extension calls this API over HTTP (default `http://127.0.0.1:37140` when your editor runs on that same host or via port-forward).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `anythingllm.baseUrl` | Base URL without trailing slash (default matches `deploy/nginx/README-ia-enso.md`). |
|
| `anythingllm.baseUrl` | Base URL without trailing slash (default matches `deploy/nginx/README-ia-enso.md`). |
|
||||||
| `anythingllm.apiKey` | Secret from AnythingLLM **Settings → API Keys** (optional leading `Bearer ` is stripped). Use **User** settings to avoid committing secrets. |
|
| `anythingllm.apiKey` | Secret from AnythingLLM **Settings → API Keys** (optional leading `Bearer ` is stripped). Use **User** settings. |
|
||||||
|
| `anythingllm.reposApiBaseUrl` | `repos-devtools-server` base URL (no trailing slash), default `http://127.0.0.1:37140`. |
|
||||||
|
| `anythingllm.reposApiToken` | Same value as `REPOS_DEVTOOLS_TOKEN` on the server. **User** settings. |
|
||||||
|
|
||||||
## Commands
|
## Commands (palette)
|
||||||
|
|
||||||
- **AnythingLLM: List workspaces** — Fetches workspaces, then opens the selected one in the default browser (`/workspace/<slug>` under your base URL).
|
- **AnythingLLM: List workspaces** — `GET /api/v1/workspaces`, then open the chosen workspace in the browser.
|
||||||
- **AnythingLLM: Open web UI** — Opens the AnythingLLM base URL.
|
- **AnythingLLM: Open web UI** — Opens the AnythingLLM base URL.
|
||||||
|
- **AnythingLLM: Dev tools panel** — Webview: enter commands, **Run**, read JSON/text **Response**.
|
||||||
|
|
||||||
|
## Dev tools panel — command lines
|
||||||
|
|
||||||
|
One command per line:
|
||||||
|
|
||||||
|
| Line | Behaviour |
|
||||||
|
|------|-----------|
|
||||||
|
| `/repos-clone <git-url>` | `POST /repos-clone` — clone into `REPOS_DEVTOOLS_ROOT`, branch **`test`** (override with JSON only via API, not this line). |
|
||||||
|
| `/repos-clone-sync <url>` | Clone + ensure AnythingLLM workspace named like the repo folder + **Open folder** + open workspace in browser. |
|
||||||
|
| `repos-list` or `/repos-list` | `GET /repos-list` — cloned git directories under the root. |
|
||||||
|
| `/repos-load <name>` | Verify repo folder + **Open folder** in the editor. |
|
||||||
|
| `/repos-load-sync <name>` | Open folder + ensure workspace + browser. |
|
||||||
|
| `/workspace-load <name>` | Ensure workspace exists (`GET /workspaces` then `POST /workspace/new` if missing) + browser. |
|
||||||
|
| `help` | Short built-in help text. |
|
||||||
|
|
||||||
|
**Workspace matching:** name or slug must equal the folder/repo name you use (exact string match on `name` or `slug` from AnythingLLM). New workspaces are created with `POST /api/v1/workspace/new` and `{ "name": "<name>" }`.
|
||||||
|
|
||||||
|
**AnythingLLM “user”:** the developer API uses the **API key**; per-user workspace ownership follows AnythingLLM’s own multi-user rules — this extension does not impersonate a browser session.
|
||||||
|
|
||||||
## Ollama
|
## Ollama
|
||||||
|
|
||||||
This extension targets **AnythingLLM** only. For OpenAI-compatible Ollama behind the same proxy, use Cursor’s model settings with `https://ia.enso.4nkweb.com/ollama/v1` and the nginx Bearer (see `deploy/nginx/README-ia-enso.md`).
|
This extension does not call Ollama. For `https://ia.enso.4nkweb.com/ollama/v1`, use Cursor model settings and the nginx Bearer (see `deploy/nginx/README-ia-enso.md`).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@ -37,4 +60,5 @@ Load the folder in VS Code / Cursor with **Run Extension** or install the packag
|
|||||||
|
|
||||||
## API reference
|
## API reference
|
||||||
|
|
||||||
Upstream routes (mounted under `/api`): `GET /v1/workspaces` — see Mintplex-Labs anything-llm `server/endpoints/api/workspace/index.js`.
|
- AnythingLLM: `server/endpoints/api/workspace/index.js` (e.g. `GET /v1/workspaces`, `POST /v1/workspace/new` under `/api`).
|
||||||
|
- Local repos: `services/repos-devtools-server/README.md`.
|
||||||
|
|||||||
25
extensions/anythingllm-workspaces/media/devTools.js
Normal file
25
extensions/anythingllm-workspaces/media/devTools.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
(function () {
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
const input = document.getElementById("cmd");
|
||||||
|
const out = document.getElementById("out");
|
||||||
|
const runBtn = document.getElementById("run");
|
||||||
|
const clearBtn = document.getElementById("clear");
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const text = input.value;
|
||||||
|
out.textContent = "…";
|
||||||
|
vscode.postMessage({ type: "run", text: text });
|
||||||
|
}
|
||||||
|
|
||||||
|
runBtn.addEventListener("click", run);
|
||||||
|
clearBtn.addEventListener("click", function () {
|
||||||
|
out.textContent = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("message", function (event) {
|
||||||
|
const msg = event.data;
|
||||||
|
if (msg && msg.type === "result") {
|
||||||
|
out.textContent = typeof msg.text === "string" ? msg.text : String(msg.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "anythingllm-workspaces",
|
"name": "anythingllm-workspaces",
|
||||||
"displayName": "AnythingLLM Workspaces (ia.enso)",
|
"displayName": "AnythingLLM Workspaces (ia.enso)",
|
||||||
"description": "List AnythingLLM workspaces via your proxied instance (e.g. ia.enso.4nkweb.com/anythingllm).",
|
"description": "List AnythingLLM workspaces via your proxied instance (e.g. ia.enso.4nkweb.com/anythingllm).",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"publisher": "4nk",
|
"publisher": "4nk",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -28,6 +28,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"default": "",
|
||||||
"markdownDescription": "AnythingLLM API key (UI: **Settings → API Keys**). Prefer **User** settings to avoid committing secrets."
|
"markdownDescription": "AnythingLLM API key (UI: **Settings → API Keys**). Prefer **User** settings to avoid committing secrets."
|
||||||
|
},
|
||||||
|
"anythingllm.reposApiBaseUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "http://127.0.0.1:37140",
|
||||||
|
"markdownDescription": "Base URL of **repos-devtools-server** (no trailing slash). Must match the machine where `/home/ncantu/code` (or `REPOS_DEVTOOLS_ROOT`) lives."
|
||||||
|
},
|
||||||
|
"anythingllm.reposApiToken": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"markdownDescription": "Bearer token shared with `REPOS_DEVTOOLS_TOKEN` on the repos-devtools-server. **User** settings only."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -39,6 +49,10 @@
|
|||||||
{
|
{
|
||||||
"command": "anythingllm.openWebUi",
|
"command": "anythingllm.openWebUi",
|
||||||
"title": "AnythingLLM: Open web UI"
|
"title": "AnythingLLM: Open web UI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "anythingllm.openDevToolsPanel",
|
||||||
|
"title": "AnythingLLM: Dev tools panel"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -80,3 +80,47 @@ export const listWorkspaces = async (
|
|||||||
}
|
}
|
||||||
return parseListWorkspaces(parseJson(text));
|
return parseListWorkspaces(parseJson(text));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseWorkspaceEnvelope = (payload: unknown): AnythingWorkspace => {
|
||||||
|
if (!isRecord(payload)) {
|
||||||
|
throw new Error("AnythingLLM API: expected object body");
|
||||||
|
}
|
||||||
|
const ws = payload.workspace;
|
||||||
|
if (!isWorkspace(ws)) {
|
||||||
|
throw new Error("AnythingLLM API: missing workspace in response");
|
||||||
|
}
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createWorkspace = async (
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<AnythingWorkspace> => {
|
||||||
|
const normalized = normalizeAnythingLlmBaseUrl(baseUrl);
|
||||||
|
const key = normalizeApiSecret(apiKey);
|
||||||
|
if (key.length === 0) {
|
||||||
|
throw new Error("anythingllm.apiKey is empty");
|
||||||
|
}
|
||||||
|
const label = name.trim();
|
||||||
|
if (label.length === 0) {
|
||||||
|
throw new Error("workspace name is empty");
|
||||||
|
}
|
||||||
|
const url = `${normalized}/api/v1/workspace/new`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name: label }),
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`AnythingLLM API ${response.status}: ${text.slice(0, 500)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parseWorkspaceEnvelope(parseJson(text));
|
||||||
|
};
|
||||||
|
|||||||
52
extensions/anythingllm-workspaces/src/commandParser.ts
Normal file
52
extensions/anythingllm-workspaces/src/commandParser.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export type ParsedDevCommand =
|
||||||
|
| { readonly kind: "repos-clone"; readonly url: string; readonly sync: boolean }
|
||||||
|
| { readonly kind: "repos-list" }
|
||||||
|
| { readonly kind: "repos-load"; readonly name: string; readonly sync: boolean }
|
||||||
|
| { readonly kind: "workspace-load"; readonly name: string }
|
||||||
|
| { readonly kind: "help" }
|
||||||
|
| { readonly kind: "unknown"; readonly raw: string };
|
||||||
|
|
||||||
|
export const parseDevCommandLine = (line: string): ParsedDevCommand => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return { kind: "unknown", raw: line };
|
||||||
|
}
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
const cmd = parts[0];
|
||||||
|
const argRest = parts.slice(1).join(" ").trim();
|
||||||
|
if (cmd === "/repos-clone-sync") {
|
||||||
|
return { kind: "repos-clone", url: argRest, sync: true };
|
||||||
|
}
|
||||||
|
if (cmd === "/repos-clone") {
|
||||||
|
return { kind: "repos-clone", url: argRest, sync: false };
|
||||||
|
}
|
||||||
|
if (cmd === "repos-list" || cmd === "/repos-list") {
|
||||||
|
return { kind: "repos-list" };
|
||||||
|
}
|
||||||
|
if (cmd === "/repos-load-sync") {
|
||||||
|
return { kind: "repos-load", name: argRest, sync: true };
|
||||||
|
}
|
||||||
|
if (cmd === "/repos-load") {
|
||||||
|
return { kind: "repos-load", name: argRest, sync: false };
|
||||||
|
}
|
||||||
|
if (cmd === "/workspace-load") {
|
||||||
|
return { kind: "workspace-load", name: argRest };
|
||||||
|
}
|
||||||
|
if (cmd === "help" || cmd === "/help") {
|
||||||
|
return { kind: "help" };
|
||||||
|
}
|
||||||
|
return { kind: "unknown", raw: trimmed };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const devCommandsHelpText = (): string => {
|
||||||
|
return [
|
||||||
|
"Commands (one per line):",
|
||||||
|
" /repos-clone <git-url> — clone into REPOS_DEVTOOLS_ROOT, branch test (default)",
|
||||||
|
" /repos-clone-sync <url> — clone + ensure AnythingLLM workspace (same name) + open folder + browser",
|
||||||
|
" repos-list — list cloned git repos under root",
|
||||||
|
" /repos-load <name> — verify repo; open folder in editor",
|
||||||
|
" /repos-load-sync <name> — open folder + ensure workspace + browser",
|
||||||
|
" /workspace-load <name> — ensure workspace by name (create via API if missing) + browser",
|
||||||
|
" help — this list",
|
||||||
|
].join("\n");
|
||||||
|
};
|
||||||
36
extensions/anythingllm-workspaces/src/config.ts
Normal file
36
extensions/anythingllm-workspaces/src/config.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
|
||||||
|
const CONFIG_SECTION = "anythingllm";
|
||||||
|
|
||||||
|
export interface DevToolsConfigSnapshot {
|
||||||
|
readonly anythingBaseUrl: string;
|
||||||
|
readonly anythingApiKey: string;
|
||||||
|
readonly reposApiBaseUrl: string;
|
||||||
|
readonly reposApiToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readAnythingConfig = (): { baseUrl: string; apiKey: string } => {
|
||||||
|
const cfg = vscode.workspace.getConfiguration(CONFIG_SECTION);
|
||||||
|
const baseUrl = typeof cfg.get("baseUrl") === "string" ? (cfg.get("baseUrl") as string) : "";
|
||||||
|
const apiKey = typeof cfg.get("apiKey") === "string" ? (cfg.get("apiKey") as string) : "";
|
||||||
|
return { baseUrl, apiKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readDevToolsConfig = (): DevToolsConfigSnapshot => {
|
||||||
|
const cfg = vscode.workspace.getConfiguration(CONFIG_SECTION);
|
||||||
|
const { baseUrl, apiKey } = readAnythingConfig();
|
||||||
|
const reposApiBaseUrl =
|
||||||
|
typeof cfg.get("reposApiBaseUrl") === "string"
|
||||||
|
? (cfg.get("reposApiBaseUrl") as string)
|
||||||
|
: "";
|
||||||
|
const reposApiToken =
|
||||||
|
typeof cfg.get("reposApiToken") === "string"
|
||||||
|
? (cfg.get("reposApiToken") as string)
|
||||||
|
: "";
|
||||||
|
return {
|
||||||
|
anythingBaseUrl: baseUrl,
|
||||||
|
anythingApiKey: apiKey,
|
||||||
|
reposApiBaseUrl,
|
||||||
|
reposApiToken,
|
||||||
|
};
|
||||||
|
};
|
||||||
196
extensions/anythingllm-workspaces/src/devToolsExecutor.ts
Normal file
196
extensions/anythingllm-workspaces/src/devToolsExecutor.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import type * as vscode from "vscode";
|
||||||
|
import {
|
||||||
|
devCommandsHelpText,
|
||||||
|
parseDevCommandLine,
|
||||||
|
type ParsedDevCommand,
|
||||||
|
} from "./commandParser";
|
||||||
|
import { normalizeAnythingLlmBaseUrl } from "./anythingllmClient";
|
||||||
|
import { reposApiClone, reposApiList, reposApiLoad } from "./reposApiClient";
|
||||||
|
import { ensureWorkspaceForRepoName } from "./workspaceEnsure";
|
||||||
|
|
||||||
|
const DEFAULT_BRANCH = "test";
|
||||||
|
|
||||||
|
const fmt = (value: unknown): string => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DevToolsRunnerContext {
|
||||||
|
readonly anythingBaseUrl: string;
|
||||||
|
readonly anythingApiKey: string;
|
||||||
|
readonly reposApiBaseUrl: string;
|
||||||
|
readonly reposApiToken: string;
|
||||||
|
readonly openFolder: (fsPath: string) => Thenable<void>;
|
||||||
|
readonly openAnythingWorkspaceInBrowser: (slug: string) => Thenable<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assertReposConfig = (ctx: DevToolsRunnerContext): void => {
|
||||||
|
if (ctx.reposApiBaseUrl.trim().length === 0) {
|
||||||
|
throw new Error("Set anythingllm.reposApiBaseUrl (repos-devtools-server URL).");
|
||||||
|
}
|
||||||
|
if (ctx.reposApiToken.trim().length === 0) {
|
||||||
|
throw new Error("Set anythingllm.reposApiToken (same value as REPOS_DEVTOOLS_TOKEN).");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertAnythingConfig = (ctx: DevToolsRunnerContext): void => {
|
||||||
|
if (ctx.anythingApiKey.trim().length === 0) {
|
||||||
|
throw new Error("Set anythingllm.apiKey for workspace operations.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runOne = async (
|
||||||
|
cmd: ParsedDevCommand,
|
||||||
|
ctx: DevToolsRunnerContext,
|
||||||
|
): Promise<string> => {
|
||||||
|
if (cmd.kind === "help") {
|
||||||
|
return devCommandsHelpText();
|
||||||
|
}
|
||||||
|
if (cmd.kind === "unknown") {
|
||||||
|
return `Unknown command: ${cmd.raw}\n${devCommandsHelpText()}`;
|
||||||
|
}
|
||||||
|
if (cmd.kind === "repos-list") {
|
||||||
|
assertReposConfig(ctx);
|
||||||
|
const data = await reposApiList(ctx.reposApiBaseUrl, ctx.reposApiToken);
|
||||||
|
return fmt(data);
|
||||||
|
}
|
||||||
|
if (cmd.kind === "repos-clone") {
|
||||||
|
assertReposConfig(ctx);
|
||||||
|
if (cmd.url.length === 0) {
|
||||||
|
throw new Error("/repos-clone requires a git URL.");
|
||||||
|
}
|
||||||
|
const data = await reposApiClone(
|
||||||
|
ctx.reposApiBaseUrl,
|
||||||
|
ctx.reposApiToken,
|
||||||
|
cmd.url,
|
||||||
|
DEFAULT_BRANCH,
|
||||||
|
);
|
||||||
|
let out = fmt(data);
|
||||||
|
if (cmd.sync) {
|
||||||
|
assertAnythingConfig(ctx);
|
||||||
|
const rec = data as Record<string, unknown>;
|
||||||
|
const name = rec.name;
|
||||||
|
const fsPath = rec.path;
|
||||||
|
if (typeof name !== "string") {
|
||||||
|
throw new Error("clone response missing name");
|
||||||
|
}
|
||||||
|
if (typeof fsPath !== "string" || fsPath.length === 0) {
|
||||||
|
throw new Error("clone response missing path");
|
||||||
|
}
|
||||||
|
const ensured = await ensureWorkspaceForRepoName(
|
||||||
|
ctx.anythingBaseUrl,
|
||||||
|
ctx.anythingApiKey,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
out += `\n---\nAnythingLLM workspace: ${fmt({
|
||||||
|
slug: ensured.workspace.slug,
|
||||||
|
name: ensured.workspace.name,
|
||||||
|
created: ensured.created,
|
||||||
|
})}`;
|
||||||
|
await ctx.openFolder(fsPath);
|
||||||
|
await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (cmd.kind === "repos-load") {
|
||||||
|
assertReposConfig(ctx);
|
||||||
|
if (cmd.name.length === 0) {
|
||||||
|
throw new Error("/repos-load requires a repository folder name.");
|
||||||
|
}
|
||||||
|
const loaded = await reposApiLoad(
|
||||||
|
ctx.reposApiBaseUrl,
|
||||||
|
ctx.reposApiToken,
|
||||||
|
cmd.name,
|
||||||
|
);
|
||||||
|
let out = fmt(loaded);
|
||||||
|
await ctx.openFolder(loaded.path);
|
||||||
|
if (cmd.sync) {
|
||||||
|
assertAnythingConfig(ctx);
|
||||||
|
const ensured = await ensureWorkspaceForRepoName(
|
||||||
|
ctx.anythingBaseUrl,
|
||||||
|
ctx.anythingApiKey,
|
||||||
|
loaded.name,
|
||||||
|
);
|
||||||
|
out += `\n---\nAnythingLLM workspace: ${fmt({
|
||||||
|
slug: ensured.workspace.slug,
|
||||||
|
name: ensured.workspace.name,
|
||||||
|
created: ensured.created,
|
||||||
|
})}`;
|
||||||
|
await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (cmd.kind === "workspace-load") {
|
||||||
|
assertAnythingConfig(ctx);
|
||||||
|
if (cmd.name.length === 0) {
|
||||||
|
throw new Error("/workspace-load requires a workspace name.");
|
||||||
|
}
|
||||||
|
const ensured = await ensureWorkspaceForRepoName(
|
||||||
|
ctx.anythingBaseUrl,
|
||||||
|
ctx.anythingApiKey,
|
||||||
|
cmd.name,
|
||||||
|
);
|
||||||
|
await ctx.openAnythingWorkspaceInBrowser(ensured.workspace.slug);
|
||||||
|
return fmt({
|
||||||
|
slug: ensured.workspace.slug,
|
||||||
|
name: ensured.workspace.name,
|
||||||
|
created: ensured.created,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `Unhandled: ${JSON.stringify(cmd)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runDevToolsScript = async (
|
||||||
|
text: string,
|
||||||
|
ctx: DevToolsRunnerContext,
|
||||||
|
): Promise<string> => {
|
||||||
|
const lines = text
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l.length > 0);
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return devCommandsHelpText();
|
||||||
|
}
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
const parsed = parseDevCommandLine(line);
|
||||||
|
try {
|
||||||
|
parts.push(await runOne(parsed, ctx));
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
parts.push(`ERROR: ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join("\n---\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerDevToolsOpenFolder = (
|
||||||
|
vscodeApi: typeof vscode,
|
||||||
|
): ((fsPath: string) => Thenable<void>) => {
|
||||||
|
return (fsPath: string) => {
|
||||||
|
if (fsPath.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const uri = vscodeApi.Uri.file(fsPath);
|
||||||
|
return vscodeApi.commands.executeCommand("vscode.openFolder", uri, false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeOpenAnythingHandler = (
|
||||||
|
vscodeApi: typeof vscode,
|
||||||
|
baseUrl: string,
|
||||||
|
): ((slug: string) => Thenable<void>) => {
|
||||||
|
return (slug: string) => {
|
||||||
|
const root = normalizeAnythingLlmBaseUrl(baseUrl);
|
||||||
|
const uri = vscodeApi.Uri.parse(
|
||||||
|
`${root}/workspace/${encodeURIComponent(slug)}`,
|
||||||
|
);
|
||||||
|
return vscodeApi.env.openExternal(uri).then(() => undefined);
|
||||||
|
};
|
||||||
|
};
|
||||||
103
extensions/anythingllm-workspaces/src/devToolsPanel.ts
Normal file
103
extensions/anythingllm-workspaces/src/devToolsPanel.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import * as vscode from "vscode";
|
||||||
|
import type { DevToolsConfigSnapshot } from "./config";
|
||||||
|
import { runDevToolsScript, makeOpenAnythingHandler, registerDevToolsOpenFolder } from "./devToolsExecutor";
|
||||||
|
|
||||||
|
const PANEL_ID = "anythingllmDevTools";
|
||||||
|
|
||||||
|
const panelTitle = "AnythingLLM dev tools";
|
||||||
|
|
||||||
|
const buildHtml = (
|
||||||
|
webview: vscode.Webview,
|
||||||
|
extensionUri: vscode.Uri,
|
||||||
|
): string => {
|
||||||
|
const scriptUri = webview.asWebviewUri(
|
||||||
|
vscode.Uri.joinPath(extensionUri, "media", "devTools.js"),
|
||||||
|
);
|
||||||
|
const csp = webview.cspSource;
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${csp} 'unsafe-inline'; script-src ${csp};" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${panelTitle}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 0.75rem; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
|
||||||
|
label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
|
||||||
|
textarea#cmd { width: 100%; min-height: 7rem; box-sizing: border-box; font-family: monospace; font-size: 12px; }
|
||||||
|
pre#out { white-space: pre-wrap; word-break: break-word; min-height: 4rem; padding: 0.5rem; border: 1px solid var(--vscode-panel-border); }
|
||||||
|
.row { margin-bottom: 0.5rem; }
|
||||||
|
button { margin-right: 0.5rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="row">
|
||||||
|
<label for="cmd">Commands (one per line)</label>
|
||||||
|
<textarea id="cmd" spellcheck="false" placeholder="/repos-clone https://… repos-list"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" id="run">Run</button>
|
||||||
|
<button type="button" id="clear">Clear output</button>
|
||||||
|
</div>
|
||||||
|
<label for="out">Response</label>
|
||||||
|
<pre id="out"></pre>
|
||||||
|
<script src="${scriptUri}"></script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showDevToolsPanel = (
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
readConfig: () => DevToolsConfigSnapshot,
|
||||||
|
): void => {
|
||||||
|
const column = vscode.ViewColumn.Beside;
|
||||||
|
const panel = vscode.window.createWebviewPanel(
|
||||||
|
PANEL_ID,
|
||||||
|
panelTitle,
|
||||||
|
column,
|
||||||
|
{
|
||||||
|
enableScripts: true,
|
||||||
|
retainContextWhenHidden: true,
|
||||||
|
localResourceRoots: [
|
||||||
|
vscode.Uri.joinPath(context.extensionUri, "media"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.webview.html = buildHtml(panel.webview, context.extensionUri);
|
||||||
|
|
||||||
|
const openFolder = registerDevToolsOpenFolder(vscode);
|
||||||
|
|
||||||
|
panel.webview.onDidReceiveMessage(
|
||||||
|
(msg: unknown) => {
|
||||||
|
void (async () => {
|
||||||
|
if (typeof msg !== "object" || msg === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rec = msg as Record<string, unknown>;
|
||||||
|
if (rec.type !== "run") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = typeof rec.text === "string" ? rec.text : "";
|
||||||
|
const use = readConfig();
|
||||||
|
const openBrowser = makeOpenAnythingHandler(vscode, use.anythingBaseUrl);
|
||||||
|
try {
|
||||||
|
const result = await runDevToolsScript(text, {
|
||||||
|
anythingBaseUrl: use.anythingBaseUrl,
|
||||||
|
anythingApiKey: use.anythingApiKey,
|
||||||
|
reposApiBaseUrl: use.reposApiBaseUrl,
|
||||||
|
reposApiToken: use.reposApiToken,
|
||||||
|
openFolder,
|
||||||
|
openAnythingWorkspaceInBrowser: openBrowser,
|
||||||
|
});
|
||||||
|
panel.webview.postMessage({ type: "result", text: result });
|
||||||
|
} catch (e) {
|
||||||
|
const m = e instanceof Error ? e.message : String(e);
|
||||||
|
panel.webview.postMessage({ type: "result", text: `ERROR: ${m}` });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
context.subscriptions,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,15 +1,8 @@
|
|||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
|
import { readAnythingConfig, readDevToolsConfig } from "./config";
|
||||||
import { listWorkspaces, normalizeAnythingLlmBaseUrl } from "./anythingllmClient";
|
import { listWorkspaces, normalizeAnythingLlmBaseUrl } from "./anythingllmClient";
|
||||||
import type { AnythingWorkspace } from "./types";
|
import type { AnythingWorkspace } from "./types";
|
||||||
|
import { showDevToolsPanel } from "./devToolsPanel";
|
||||||
const CONFIG_SECTION = "anythingllm";
|
|
||||||
|
|
||||||
const readConfig = (): { baseUrl: string; apiKey: string } => {
|
|
||||||
const cfg = vscode.workspace.getConfiguration(CONFIG_SECTION);
|
|
||||||
const baseUrl = typeof cfg.get("baseUrl") === "string" ? cfg.get("baseUrl") as string : "";
|
|
||||||
const apiKey = typeof cfg.get("apiKey") === "string" ? cfg.get("apiKey") as string : "";
|
|
||||||
return { baseUrl, apiKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
const workspaceLabel = (w: AnythingWorkspace): string => `${w.name} (${w.slug})`;
|
const workspaceLabel = (w: AnythingWorkspace): string => `${w.name} (${w.slug})`;
|
||||||
|
|
||||||
@ -22,7 +15,7 @@ const openWorkspaceInBrowser = async (baseUrl: string, slug: string): Promise<vo
|
|||||||
|
|
||||||
export const activate = (context: vscode.ExtensionContext): void => {
|
export const activate = (context: vscode.ExtensionContext): void => {
|
||||||
const listCmd = vscode.commands.registerCommand("anythingllm.listWorkspaces", async () => {
|
const listCmd = vscode.commands.registerCommand("anythingllm.listWorkspaces", async () => {
|
||||||
const { baseUrl, apiKey } = readConfig();
|
const { baseUrl, apiKey } = readAnythingConfig();
|
||||||
try {
|
try {
|
||||||
const workspaces = await listWorkspaces(baseUrl, apiKey);
|
const workspaces = await listWorkspaces(baseUrl, apiKey);
|
||||||
if (workspaces.length === 0) {
|
if (workspaces.length === 0) {
|
||||||
@ -47,7 +40,7 @@ export const activate = (context: vscode.ExtensionContext): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const openUiCmd = vscode.commands.registerCommand("anythingllm.openWebUi", async () => {
|
const openUiCmd = vscode.commands.registerCommand("anythingllm.openWebUi", async () => {
|
||||||
const { baseUrl } = readConfig();
|
const { baseUrl } = readAnythingConfig();
|
||||||
try {
|
try {
|
||||||
const root = normalizeAnythingLlmBaseUrl(baseUrl);
|
const root = normalizeAnythingLlmBaseUrl(baseUrl);
|
||||||
await vscode.env.openExternal(vscode.Uri.parse(root));
|
await vscode.env.openExternal(vscode.Uri.parse(root));
|
||||||
@ -57,7 +50,11 @@ export const activate = (context: vscode.ExtensionContext): void => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
context.subscriptions.push(listCmd, openUiCmd);
|
const devPanelCmd = vscode.commands.registerCommand("anythingllm.openDevToolsPanel", () => {
|
||||||
|
showDevToolsPanel(context, readDevToolsConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(listCmd, openUiCmd, devPanelCmd);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deactivate = (): void => {
|
export const deactivate = (): void => {
|
||||||
|
|||||||
94
extensions/anythingllm-workspaces/src/reposApiClient.ts
Normal file
94
extensions/anythingllm-workspaces/src/reposApiClient.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
const trimSlash = (u: string): string => u.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
const normalizeReposToken = (raw: string): string => {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
const bearerPrefix = /^Bearer\s+/i;
|
||||||
|
return bearerPrefix.test(trimmed) ? trimmed.replace(bearerPrefix, "").trim() : trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const authHeaders = (token: string): Record<string, string> => {
|
||||||
|
const key = normalizeReposToken(token);
|
||||||
|
return {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reposApiClone = async (
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
url: string,
|
||||||
|
branch: string,
|
||||||
|
): Promise<unknown> => {
|
||||||
|
const root = trimSlash(baseUrl.trim());
|
||||||
|
if (root.length === 0) {
|
||||||
|
throw new Error("anythingllm.reposApiBaseUrl is empty");
|
||||||
|
}
|
||||||
|
const res = await fetch(`${root}/repos-clone`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: authHeaders(token),
|
||||||
|
body: JSON.stringify({ url, branch }),
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
let body: unknown = text;
|
||||||
|
try {
|
||||||
|
body = JSON.parse(text) as unknown;
|
||||||
|
} catch {
|
||||||
|
/* keep text */
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`repos API ${res.status}: ${typeof body === "string" ? body : JSON.stringify(body)}`);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reposApiList = async (baseUrl: string, token: string): Promise<unknown> => {
|
||||||
|
const root = trimSlash(baseUrl.trim());
|
||||||
|
if (root.length === 0) {
|
||||||
|
throw new Error("anythingllm.reposApiBaseUrl is empty");
|
||||||
|
}
|
||||||
|
const res = await fetch(`${root}/repos-list`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${normalizeReposToken(token)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`repos API ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
return JSON.parse(text) as unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reposApiLoad = async (
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<{ path: string; name: string }> => {
|
||||||
|
const root = trimSlash(baseUrl.trim());
|
||||||
|
if (root.length === 0) {
|
||||||
|
throw new Error("anythingllm.reposApiBaseUrl is empty");
|
||||||
|
}
|
||||||
|
const res = await fetch(`${root}/repos-load`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: authHeaders(token),
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
const body = JSON.parse(text) as unknown;
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`repos API ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
if (typeof body !== "object" || body === null) {
|
||||||
|
throw new Error("repos-load: invalid response");
|
||||||
|
}
|
||||||
|
const rec = body as Record<string, unknown>;
|
||||||
|
const path = rec.path;
|
||||||
|
const n = rec.name;
|
||||||
|
if (typeof path !== "string" || typeof n !== "string") {
|
||||||
|
throw new Error("repos-load: missing path or name");
|
||||||
|
}
|
||||||
|
return { path, name: n };
|
||||||
|
};
|
||||||
27
extensions/anythingllm-workspaces/src/workspaceEnsure.ts
Normal file
27
extensions/anythingllm-workspaces/src/workspaceEnsure.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createWorkspace, listWorkspaces } from "./anythingllmClient";
|
||||||
|
import type { AnythingWorkspace } from "./types";
|
||||||
|
|
||||||
|
export interface EnsuredWorkspace {
|
||||||
|
readonly workspace: AnythingWorkspace;
|
||||||
|
readonly created: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ensureWorkspaceForRepoName = async (
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey: string,
|
||||||
|
repoName: string,
|
||||||
|
): Promise<EnsuredWorkspace> => {
|
||||||
|
const key = repoName.trim();
|
||||||
|
if (key.length === 0) {
|
||||||
|
throw new Error("repo/workspace name is empty");
|
||||||
|
}
|
||||||
|
const all = await listWorkspaces(baseUrl, apiKey);
|
||||||
|
const byName = all.find((w) => w.name === key);
|
||||||
|
const bySlug = all.find((w) => w.slug === key);
|
||||||
|
const found = byName ?? bySlug;
|
||||||
|
if (found) {
|
||||||
|
return { workspace: found, created: false };
|
||||||
|
}
|
||||||
|
const created = await createWorkspace(baseUrl, apiKey, key);
|
||||||
|
return { workspace: created, created: true };
|
||||||
|
};
|
||||||
2
services/repos-devtools-server/.gitignore
vendored
Normal file
2
services/repos-devtools-server/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
36
services/repos-devtools-server/README.md
Normal file
36
services/repos-devtools-server/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# repos-devtools-server
|
||||||
|
|
||||||
|
Local HTTP API bound to **`127.0.0.1`** for git operations under **`REPOS_DEVTOOLS_ROOT`** (default `/home/ncantu/code`).
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `REPOS_DEVTOOLS_TOKEN` | yes | Shared secret; clients send `Authorization: Bearer <token>`. |
|
||||||
|
| `REPOS_DEVTOOLS_ROOT` | no | Absolute root for clones (default `/home/ncantu/code`). |
|
||||||
|
| `REPOS_DEVTOOLS_HOST` | no | Bind address (default `127.0.0.1`). |
|
||||||
|
| `REPOS_DEVTOOLS_PORT` | no | Port (default `37140`). |
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- `POST /repos-clone` — JSON `{ "url": "<git url>", "branch": "test" }` (`branch` optional, default `test`).
|
||||||
|
- `GET /repos-list` — Lists immediate subdirectories of the root that contain `.git`.
|
||||||
|
- `POST /repos-load` — JSON `{ "name": "<folder name>" }` — Verifies the repo exists; returns absolute `path`.
|
||||||
|
|
||||||
|
All endpoints require `Authorization: Bearer <REPOS_DEVTOOLS_TOKEN>`.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/repos-devtools-server
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
export REPOS_DEVTOOLS_TOKEN='generate-a-long-random-secret'
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the same token in the VS Code / Cursor setting **`anythingllm.reposApiToken`**.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
The **AnythingLLM Workspaces** extension command **AnythingLLM: Dev tools panel** calls this API and the AnythingLLM HTTP API for workspace create/list.
|
||||||
51
services/repos-devtools-server/package-lock.json
generated
Normal file
51
services/repos-devtools-server/package-lock.json
generated
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@4nk/repos-devtools-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@4nk/repos-devtools-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.19.37",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||||
|
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
services/repos-devtools-server/package.json
Normal file
20
services/repos-devtools-server/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@4nk/repos-devtools-server",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Local HTTP API: git clone/list under REPOS_DEVTOOLS_ROOT (e.g. /home/ncantu/code).",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/server.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p ./",
|
||||||
|
"start": "node dist/server.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
services/repos-devtools-server/src/auth.ts
Normal file
21
services/repos-devtools-server/src/auth.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
|
||||||
|
export const readExpectedToken = (): string => {
|
||||||
|
return process.env.REPOS_DEVTOOLS_TOKEN?.trim() ?? "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireBearer = (
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
expected: string,
|
||||||
|
): boolean => {
|
||||||
|
const h = req.headers.authorization ?? "";
|
||||||
|
const match = /^Bearer\s+(.+)$/i.exec(h);
|
||||||
|
const got = match?.[1]?.trim() ?? "";
|
||||||
|
if (got !== expected) {
|
||||||
|
res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
34
services/repos-devtools-server/src/gitSpawn.ts
Normal file
34
services/repos-devtools-server/src/gitSpawn.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
|
export interface GitSpawnResult {
|
||||||
|
readonly code: number;
|
||||||
|
readonly stdout: string;
|
||||||
|
readonly stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runGit = (args: readonly string[], cwd: string): Promise<GitSpawnResult> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("git", [...args], {
|
||||||
|
cwd,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
const out: Buffer[] = [];
|
||||||
|
const err: Buffer[] = [];
|
||||||
|
child.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
out.push(chunk);
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
err.push(chunk);
|
||||||
|
});
|
||||||
|
child.on("error", (e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
resolve({
|
||||||
|
code: code ?? 1,
|
||||||
|
stdout: Buffer.concat(out).toString("utf8"),
|
||||||
|
stderr: Buffer.concat(err).toString("utf8"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
130
services/repos-devtools-server/src/handlers.ts
Normal file
130
services/repos-devtools-server/src/handlers.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
json(res, 200, {
|
||||||
|
ok: true,
|
||||||
|
name,
|
||||||
|
path: dest,
|
||||||
|
branch,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 });
|
||||||
|
};
|
||||||
25
services/repos-devtools-server/src/httpUtil.ts
Normal file
25
services/repos-devtools-server/src/httpUtil.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import type { IncomingMessage } from "node:http";
|
||||||
|
|
||||||
|
const MAX_BODY = 1_048_576;
|
||||||
|
|
||||||
|
export const readJsonBody = async (req: IncomingMessage): Promise<unknown> => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let total = 0;
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
total += buf.length;
|
||||||
|
if (total > MAX_BODY) {
|
||||||
|
throw new Error("Request body too large");
|
||||||
|
}
|
||||||
|
chunks.push(buf);
|
||||||
|
}
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
||||||
|
if (raw.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as unknown;
|
||||||
|
} catch (cause) {
|
||||||
|
throw new Error("Invalid JSON body", { cause });
|
||||||
|
}
|
||||||
|
};
|
||||||
47
services/repos-devtools-server/src/paths.ts
Normal file
47
services/repos-devtools-server/src/paths.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$/;
|
||||||
|
|
||||||
|
export const assertSafeRepoName = (name: string): string => {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (!SAFE_NAME.test(trimmed)) {
|
||||||
|
throw new Error("Invalid repo name: use letters, digits, . _ - only; no path segments.");
|
||||||
|
}
|
||||||
|
if (trimmed.includes("..")) {
|
||||||
|
throw new Error("Invalid repo name: path traversal rejected.");
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCodeRoot = (): string => {
|
||||||
|
const raw = process.env.REPOS_DEVTOOLS_ROOT ?? "/home/ncantu/code";
|
||||||
|
const resolved = path.resolve(raw);
|
||||||
|
return resolved;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const repoDirForName = (codeRoot: string, name: string): string => {
|
||||||
|
const safe = assertSafeRepoName(name);
|
||||||
|
return path.join(codeRoot, safe);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isGitRepo = async (dir: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(path.join(dir, ".git"));
|
||||||
|
return stat.isDirectory() || stat.isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const repoNameFromGitUrl = (rawUrl: string): string => {
|
||||||
|
const trimmed = rawUrl.trim();
|
||||||
|
const noQuery = trimmed.split("?")[0] ?? trimmed;
|
||||||
|
const base = noQuery.replace(/[/]+$/, "");
|
||||||
|
const segment = base.split("/").filter((s) => s.length > 0).pop() ?? "";
|
||||||
|
const withoutGit = segment.replace(/\.git$/i, "");
|
||||||
|
if (withoutGit.length === 0) {
|
||||||
|
throw new Error("Could not derive repository name from URL.");
|
||||||
|
}
|
||||||
|
return assertSafeRepoName(withoutGit);
|
||||||
|
};
|
||||||
56
services/repos-devtools-server/src/server.ts
Normal file
56
services/repos-devtools-server/src/server.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as http from "node:http";
|
||||||
|
import { requireBearer, readExpectedToken } from "./auth.js";
|
||||||
|
import {
|
||||||
|
handleReposClone,
|
||||||
|
handleReposList,
|
||||||
|
handleReposLoad,
|
||||||
|
} from "./handlers.js";
|
||||||
|
|
||||||
|
const HOST = process.env.REPOS_DEVTOOLS_HOST ?? "127.0.0.1";
|
||||||
|
const PORT = Number(process.env.REPOS_DEVTOOLS_PORT ?? "37140");
|
||||||
|
|
||||||
|
const main = (): void => {
|
||||||
|
const token = readExpectedToken();
|
||||||
|
if (token.length === 0) {
|
||||||
|
console.error("repos-devtools-server: set REPOS_DEVTOOLS_TOKEN (non-empty secret).");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
if (!requireBearer(req, res, token)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(req.url ?? "/", `http://${HOST}`);
|
||||||
|
const p = url.pathname;
|
||||||
|
if (req.method === "POST" && p === "/repos-clone") {
|
||||||
|
await handleReposClone(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === "GET" && p === "/repos-list") {
|
||||||
|
await handleReposList(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === "POST" && p === "/repos-load") {
|
||||||
|
await handleReposLoad(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
res.end(JSON.stringify({ error: "Not found" }));
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
res.end(JSON.stringify({ error: msg }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
|
console.error(
|
||||||
|
`repos-devtools-server listening on http://${HOST}:${PORT} (root=${process.env.REPOS_DEVTOOLS_ROOT ?? "/home/ncantu/code"})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
16
services/repos-devtools-server/tsconfig.json
Normal file
16
services/repos-devtools-server/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"declaration": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user