feat: scaffold vite chrome extension
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
VITE_GITEA_BASE_URL=http://gitea.orson.tealthrone
|
||||
VITE_GITEA_TOKEN=
|
||||
VITE_GITEA_REPO_OWNER=jacob-mathison
|
||||
VITE_GITEA_REPO_NAME=mp-silma-ai-aide
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,38 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
`mp-silma-ai-aide` is a Vite + TypeScript Chrome extension scaffold. The built `dist/` directory is intended to be loaded in Chrome as an unpacked Manifest V3 extension.
|
||||
|
||||
## Commands
|
||||
|
||||
- Install dependencies: `npm install`
|
||||
- Run local Vite dev server: `npm run dev`
|
||||
- Build extension: `npm run build`
|
||||
- Preview build output: `npm run preview`
|
||||
|
||||
## Environment
|
||||
|
||||
Configuration is read from Vite environment variables:
|
||||
|
||||
- `VITE_GITEA_BASE_URL`: Base URL for the Gitea instance, for example `http://gitea.orson.tealthrone`.
|
||||
- `VITE_GITEA_TOKEN`: Gitea API token.
|
||||
- `VITE_GITEA_REPO_OWNER`: Target Gitea owner. Default: `jacob-mathison`.
|
||||
- `VITE_GITEA_REPO_NAME`: Target Gitea repository. Default: `mp-silma-ai-aide`.
|
||||
|
||||
Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing env files out of git. Update `.env.example` when adding new required variables.
|
||||
|
||||
## Extension Build Notes
|
||||
|
||||
- `vite.config.ts` emits `dist/manifest.json` during production builds.
|
||||
- `dist/` is generated output and should not be edited directly.
|
||||
- Host permissions are derived from `VITE_GITEA_BASE_URL`. Rebuild after changing the base URL.
|
||||
- Runtime API calls are centralized in `src/api.ts`.
|
||||
- Popup state is centralized in the vanilla Zustand store at `src/store.ts`.
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
- Keep the app TypeScript-first and strict.
|
||||
- Prefer small, focused modules under `src/`.
|
||||
- Do not commit secrets or generated build output.
|
||||
- Run `npm run build` before handing off code changes.
|
||||
@@ -1,3 +1,19 @@
|
||||
# mp-silma-ai-aide
|
||||
|
||||
Context-aware AI aide — project-agnostic tooling to give Silma better context for supporting any codebase or task.
|
||||
|
||||
## Development
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Build the Chrome extension:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Load the generated `dist/` folder in Chrome as an unpacked extension.
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Silma AI Aide</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
<header class="masthead">
|
||||
<div>
|
||||
<p class="eyebrow">Gitea</p>
|
||||
<h1>Silma AI Aide</h1>
|
||||
</div>
|
||||
<span id="config-badge" class="badge">Checking</span>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<dl class="config-list">
|
||||
<div>
|
||||
<dt>Base URL</dt>
|
||||
<dd id="base-url">Not configured</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Token</dt>
|
||||
<dd id="token-state">Not configured</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Target repo</dt>
|
||||
<dd id="target-repo">Not configured</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<button id="test-connection" type="button">Test connection</button>
|
||||
<p id="status" role="status">Ready.</p>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
# mp-silma-ai-aide
|
||||
|
||||
This project is a basic Vite + TypeScript Chrome extension scaffold. The extension popup reads Gitea configuration from Vite env variables, stores popup state in Zustand, and uses Axios to call the Gitea API.
|
||||
|
||||
Important files:
|
||||
|
||||
- `AGENTS.md`: Project instructions for coding agents.
|
||||
- `manifest.llm.json`: Machine-readable project metadata.
|
||||
- `.env.example`: Required environment variables.
|
||||
- `vite.config.ts`: Vite build config and generated Chrome extension manifest logic.
|
||||
- `src/api.ts`: Axios client and Gitea configuration handling.
|
||||
- `src/store.ts`: Vanilla Zustand popup state store.
|
||||
- `src/main.ts`: Popup UI behavior.
|
||||
- `index.html`: Popup document.
|
||||
|
||||
Build with `npm run build`. Load the generated `dist/` folder in Chrome as an unpacked extension. Do not edit `dist/` directly.
|
||||
|
||||
Required env variables:
|
||||
|
||||
- `VITE_GITEA_BASE_URL`
|
||||
- `VITE_GITEA_TOKEN`
|
||||
- `VITE_GITEA_REPO_OWNER`
|
||||
- `VITE_GITEA_REPO_NAME`
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"name": "mp-silma-ai-aide",
|
||||
"description": "A Vite and TypeScript Chrome extension popup for Gitea-backed Silma AI aide workflows.",
|
||||
"project_type": "chrome-extension",
|
||||
"language": "typescript",
|
||||
"package_manager": "npm",
|
||||
"entrypoints": {
|
||||
"popup_html": "index.html",
|
||||
"popup_script": "src/main.ts",
|
||||
"state_store": "src/store.ts",
|
||||
"api_client": "src/api.ts",
|
||||
"vite_config": "vite.config.ts",
|
||||
"generated_manifest": "dist/manifest.json"
|
||||
},
|
||||
"commands": {
|
||||
"install": "npm install",
|
||||
"dev": "npm run dev",
|
||||
"build": "npm run build",
|
||||
"preview": "npm run preview"
|
||||
},
|
||||
"environment": {
|
||||
"example_file": ".env.example",
|
||||
"local_file": ".env.local",
|
||||
"variables": [
|
||||
{
|
||||
"name": "VITE_GITEA_BASE_URL",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Base URL for the Gitea instance."
|
||||
},
|
||||
{
|
||||
"name": "VITE_GITEA_TOKEN",
|
||||
"required": true,
|
||||
"secret": true,
|
||||
"description": "Gitea API token used by the popup client."
|
||||
},
|
||||
{
|
||||
"name": "VITE_GITEA_REPO_OWNER",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Owner for the target Gitea repository."
|
||||
},
|
||||
{
|
||||
"name": "VITE_GITEA_REPO_NAME",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Name of the target Gitea repository."
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_paths": [
|
||||
"dist/",
|
||||
"node_modules/"
|
||||
],
|
||||
"agent_guidance": "See AGENTS.md for coding and build instructions. Do not edit dist/ directly; rebuild from source instead."
|
||||
}
|
||||
Generated
+1710
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "mp-silma-ai-aide",
|
||||
"version": "1.0.0",
|
||||
"description": "Chrome extension popup built with Vite for Silma AI aide workflows.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.2.2",
|
||||
"@types/node": "^26.1.1",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.1.5"
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import axios from "axios";
|
||||
|
||||
const rawGiteaBaseUrl = import.meta.env.VITE_GITEA_BASE_URL?.trim() ?? "";
|
||||
const giteaToken = import.meta.env.VITE_GITEA_TOKEN?.trim() ?? "";
|
||||
const rawGiteaRepoOwner = import.meta.env.VITE_GITEA_REPO_OWNER?.trim() ?? "";
|
||||
const rawGiteaRepoName = import.meta.env.VITE_GITEA_REPO_NAME?.trim() ?? "";
|
||||
|
||||
export interface GiteaUser {
|
||||
id: number;
|
||||
login: string;
|
||||
full_name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export const giteaConfig = {
|
||||
baseUrl: rawGiteaBaseUrl,
|
||||
hasToken: giteaToken.length > 0,
|
||||
repoOwner: rawGiteaRepoOwner,
|
||||
repoName: rawGiteaRepoName,
|
||||
targetRepo:
|
||||
rawGiteaRepoOwner.length > 0 && rawGiteaRepoName.length > 0
|
||||
? `${rawGiteaRepoOwner}/${rawGiteaRepoName}`
|
||||
: "",
|
||||
isConfigured: rawGiteaBaseUrl.length > 0 && giteaToken.length > 0
|
||||
};
|
||||
|
||||
function getApiBaseUrl(baseUrl: string): string {
|
||||
const normalized = baseUrl.replace(/\/+$/, "");
|
||||
return normalized.endsWith("/api/v1") ? normalized : `${normalized}/api/v1`;
|
||||
}
|
||||
|
||||
export async function getCurrentGiteaUser(): Promise<GiteaUser> {
|
||||
if (!giteaConfig.isConfigured) {
|
||||
throw new Error("Set VITE_GITEA_BASE_URL and VITE_GITEA_TOKEN before testing the connection.");
|
||||
}
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
|
||||
headers: {
|
||||
Authorization: `token ${giteaToken}`
|
||||
}
|
||||
});
|
||||
|
||||
const response = await client.get<GiteaUser>("/user");
|
||||
return response.data;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import "./styles.css";
|
||||
import { appStore } from "./store";
|
||||
|
||||
const configBadge = document.querySelector<HTMLSpanElement>("#config-badge");
|
||||
const baseUrl = document.querySelector<HTMLElement>("#base-url");
|
||||
const tokenState = document.querySelector<HTMLElement>("#token-state");
|
||||
const targetRepo = document.querySelector<HTMLElement>("#target-repo");
|
||||
const status = document.querySelector<HTMLElement>("#status");
|
||||
const testConnection = document.querySelector<HTMLButtonElement>("#test-connection");
|
||||
|
||||
function setText(element: HTMLElement | null, text: string): void {
|
||||
if (element) {
|
||||
element.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
const state = appStore.getState();
|
||||
const config = state.config;
|
||||
|
||||
setText(baseUrl, config.baseUrl || "Not configured");
|
||||
setText(tokenState, config.hasToken ? "Configured" : "Not configured");
|
||||
setText(targetRepo, config.targetRepo || "Not configured");
|
||||
setText(configBadge, config.isConfigured ? "Configured" : "Missing env");
|
||||
|
||||
if (configBadge) {
|
||||
configBadge.dataset.tone = config.isConfigured ? "success" : "warning";
|
||||
}
|
||||
|
||||
if (status) {
|
||||
status.textContent = state.statusMessage;
|
||||
status.dataset.tone = state.statusTone;
|
||||
}
|
||||
|
||||
if (testConnection) {
|
||||
testConnection.disabled = state.isTestingConnection;
|
||||
}
|
||||
}
|
||||
|
||||
appStore.subscribe(render);
|
||||
render();
|
||||
|
||||
testConnection?.addEventListener("click", () => {
|
||||
void appStore.getState().testConnection();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import { getCurrentGiteaUser, giteaConfig, type GiteaUser } from "./api";
|
||||
|
||||
export type StatusTone = "neutral" | "success" | "error";
|
||||
|
||||
interface AppState {
|
||||
config: typeof giteaConfig;
|
||||
connectedUser: GiteaUser | null;
|
||||
isTestingConnection: boolean;
|
||||
statusMessage: string;
|
||||
statusTone: StatusTone;
|
||||
testConnection: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const appStore = createStore<AppState>()((set) => ({
|
||||
config: giteaConfig,
|
||||
connectedUser: null,
|
||||
isTestingConnection: false,
|
||||
statusMessage: "Ready.",
|
||||
statusTone: "neutral",
|
||||
testConnection: async () => {
|
||||
set({
|
||||
connectedUser: null,
|
||||
isTestingConnection: true,
|
||||
statusMessage: "Testing connection...",
|
||||
statusTone: "neutral"
|
||||
});
|
||||
|
||||
try {
|
||||
const user = await getCurrentGiteaUser();
|
||||
set({
|
||||
connectedUser: user,
|
||||
statusMessage: `Connected as ${user.full_name || user.login}.`,
|
||||
statusTone: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Connection failed.";
|
||||
set({
|
||||
connectedUser: null,
|
||||
statusMessage: message,
|
||||
statusTone: "error"
|
||||
});
|
||||
} finally {
|
||||
set({ isTestingConnection: false });
|
||||
}
|
||||
}
|
||||
}));
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
:root {
|
||||
color: #1f2933;
|
||||
background: #f5f7fa;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 360px;
|
||||
margin: 0;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 4px;
|
||||
color: #52616f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #102a43;
|
||||
font-size: 22px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.badge {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #bcccdc;
|
||||
border-radius: 999px;
|
||||
padding: 5px 9px;
|
||||
background: #ffffff;
|
||||
color: #334e68;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge[data-tone="success"] {
|
||||
border-color: #7bc47f;
|
||||
background: #e3f9e5;
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
.badge[data-tone="warning"] {
|
||||
border-color: #f7c948;
|
||||
background: #fffbea;
|
||||
color: #8d6e00;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
border: 1px solid #d9e2ec;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgb(16 42 67 / 8%);
|
||||
}
|
||||
|
||||
.config-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.config-list div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #52616f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #102a43;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
background: #0b6bcb;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#status {
|
||||
min-height: 20px;
|
||||
margin: 0;
|
||||
color: #52616f;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#status[data-tone="success"] {
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
#status[data-tone="error"] {
|
||||
color: #b42318;
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_GITEA_BASE_URL?: string;
|
||||
readonly VITE_GITEA_TOKEN?: string;
|
||||
readonly VITE_GITEA_REPO_OWNER?: string;
|
||||
readonly VITE_GITEA_REPO_NAME?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["chrome"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { parse as parseDotenv } from "dotenv";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
|
||||
function loadExtensionEnv(mode: string): NodeJS.ProcessEnv {
|
||||
const fileEnv: Record<string, string> = {};
|
||||
|
||||
for (const fileName of [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`]) {
|
||||
const path = resolve(process.cwd(), fileName);
|
||||
|
||||
if (existsSync(path)) {
|
||||
Object.assign(fileEnv, parseDotenv(readFileSync(path)));
|
||||
}
|
||||
}
|
||||
|
||||
return { ...fileEnv, ...process.env };
|
||||
}
|
||||
|
||||
function getGiteaHostPermissions(baseUrl?: string): string[] {
|
||||
if (!baseUrl?.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(baseUrl);
|
||||
return [`${url.protocol}//${url.host}/*`];
|
||||
} catch {
|
||||
console.warn(`VITE_GITEA_BASE_URL is not a valid URL: ${baseUrl}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function chromeExtensionManifest(mode: string): Plugin {
|
||||
return {
|
||||
name: "chrome-extension-manifest",
|
||||
generateBundle() {
|
||||
const env = loadExtensionEnv(mode);
|
||||
const manifest = {
|
||||
manifest_version: 3,
|
||||
name: "Silma AI Aide",
|
||||
version: "0.1.0",
|
||||
description: "A Vite-powered popup for Gitea-backed Silma AI aide workflows.",
|
||||
action: {
|
||||
default_title: "Silma AI Aide",
|
||||
default_popup: "index.html"
|
||||
},
|
||||
permissions: ["storage"],
|
||||
host_permissions: getGiteaHostPermissions(env.VITE_GITEA_BASE_URL)
|
||||
};
|
||||
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: "manifest.json",
|
||||
source: `${JSON.stringify(manifest, null, 2)}\n`
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [chromeExtensionManifest(mode)],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
popup: resolve(__dirname, "index.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
Reference in New Issue
Block a user