diff --git a/.github/scripts/validate.ts b/.github/scripts/validate.ts index 010a6af..3620e8d 100644 --- a/.github/scripts/validate.ts +++ b/.github/scripts/validate.ts @@ -4,47 +4,15 @@ import { readFileSync, existsSync } from 'fs'; const schema = z.object({ confidence: z.number().min(0).max(1).optional(), - theme: z.string().optional(), - themes: z.record(z.string(), z.record(z.string(), z.string())).optional(), - labels: z.record(z.string(), z.string()).optional(), - rules: z + model: z.string().optional(), + examples: z .array( z.object({ - match: z.string(), - add: z.array(z.string()), + title: z.string(), + labels: z.array(z.string()), }), ) .optional(), - reactions: z - .object({ - start: z.string().optional(), - complete: z.string().optional(), - }) - .optional(), - ignore: z - .object({ - users: z.array(z.string()).optional(), - labels: z.array(z.string()).optional(), - }) - .optional(), - duplicates: z - .object({ - enabled: z.boolean().optional(), - threshold: z.number().min(0).max(1).optional(), - label: z.string().optional(), - comment: z.boolean().optional(), - close: z.boolean().optional(), - }) - .optional(), - autorespond: z - .object({ - enabled: z.boolean().optional(), - label: z.string().optional(), - context: z.string().optional(), - requirements: z.record(z.string(), z.array(z.string())).optional(), - message: z.string().optional(), - }) - .optional(), }); const configpath = '.github/tigent.yml'; diff --git a/.github/tigent.yml b/.github/tigent.yml index a5e2d1e..9763f4e 100644 --- a/.github/tigent.yml +++ b/.github/tigent.yml @@ -19,15 +19,15 @@ labels: help-wanted: muted rules: - - match: "crash|broken|not working" + - match: 'crash|broken|not working' add: [bug, p1] - - match: "security|vulnerability|cve" + - match: 'security|vulnerability|cve' add: [security, p0] - - match: "docs|readme|typo" + - match: 'docs|readme|typo' add: [documentation] - - match: "config|yaml|yml" + - match: 'config|yaml|yml' add: [config] - - match: "webhook|payload" + - match: 'webhook|payload' add: [webhook] reactions: diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..6add08e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm prettier-fix && git add -u diff --git a/app/api/webhook/feedback.ts b/app/api/webhook/feedback.ts new file mode 100644 index 0000000..d7e43f6 --- /dev/null +++ b/app/api/webhook/feedback.ts @@ -0,0 +1,126 @@ +import type { Gh, Config } from './triage'; +import { classify, fetchlabels, addlabels } from './triage'; +import { createpr } from './learn'; + +const allowed = ['OWNER', 'MEMBER', 'COLLABORATOR']; + +export async function handlecomment(gh: Gh, config: Config, payload: any) { + const comment = payload.comment; + const body: string = comment.body?.trim() || ''; + const association: string = comment.author_association || ''; + + if (!allowed.includes(association)) return; + if (!body.toLowerCase().startsWith('@tigent')) return; + + const command = body.slice(7).trim().toLowerCase(); + const issue = payload.issue; + + if (command === 'why') { + await handlewhy(gh, config, issue, comment.id); + } else if (command.startsWith('wrong')) { + const rest = body.slice(7).trim().slice(5).trim(); + const labels = parselabels(rest); + if (labels.length > 0) { + await handlewrong(gh, config, issue, comment.id, labels); + } + } +} + +async function handlewhy( + gh: Gh, + config: Config, + issue: any, + commentid: number, +) { + await reactcomment(gh, commentid); + + const labels = await fetchlabels(gh); + const result = await classify(config, labels, issue.title, issue.body || ''); + + const labelstr = result.labels.join(', '); + const body = `**labels:** ${labelstr}\n**confidence:** ${result.confidence}\n\n${result.reasoning}`; + + await gh.octokit.rest.issues.createComment({ + owner: gh.owner, + repo: gh.repo, + issue_number: issue.number, + body, + }); +} + +async function handlewrong( + gh: Gh, + config: Config, + issue: any, + commentid: number, + correctlabels: string[], +) { + await reactcomment(gh, commentid); + + const [repolabels, current] = await Promise.all([ + fetchlabels(gh), + gh.octokit.rest.issues.listLabelsOnIssue({ + owner: gh.owner, + repo: gh.repo, + issue_number: issue.number, + }), + ]); + + const result = await classify( + config, + repolabels, + issue.title, + issue.body || '', + ); + + const ailabels = result.labels; + const existing = current.data.map(l => l.name); + + for (const label of ailabels) { + if (existing.includes(label)) { + await gh.octokit.rest.issues.removeLabel({ + owner: gh.owner, + repo: gh.repo, + issue_number: issue.number, + name: label, + }); + } + } + + const validcorrect = correctlabels.filter(l => + repolabels.some(x => x.name.toLowerCase() === l.toLowerCase()), + ); + const matchedlabels = validcorrect.map(l => { + const match = repolabels.find( + x => x.name.toLowerCase() === l.toLowerCase(), + ); + return match!.name; + }); + + if (matchedlabels.length > 0) { + await addlabels(gh, issue.number, matchedlabels); + } + + await createpr(gh, issue.number, issue.title, matchedlabels); +} + +function parselabels(input: string): string[] { + const cleaned = input + .replace(/^,/, '') + .replace(/^should be/i, '') + .trim(); + if (!cleaned) return []; + return cleaned + .split(',') + .map(s => s.trim()) + .filter(Boolean); +} + +async function reactcomment(gh: Gh, commentid: number) { + await gh.octokit.rest.reactions.createForIssueComment({ + owner: gh.owner, + repo: gh.repo, + comment_id: commentid, + content: 'eyes', + }); +} diff --git a/app/api/webhook/learn.ts b/app/api/webhook/learn.ts new file mode 100644 index 0000000..0e79d6a --- /dev/null +++ b/app/api/webhook/learn.ts @@ -0,0 +1,78 @@ +import { Octokit } from 'octokit'; +import { parse, stringify } from 'yaml'; +import type { Gh, Config } from './triage'; + +const dancer = process.env.DANCER_PAT + ? new Octokit({ auth: process.env.DANCER_PAT }) + : null; + +export async function createpr( + gh: Gh, + issue: number, + title: string, + labels: string[], +) { + if (!dancer) return; + + const { data: repo } = await dancer.rest.repos.get({ + owner: gh.owner, + repo: gh.repo, + }); + const branch = `tigent/learn-${issue}`; + const defaultbranch = repo.default_branch; + + const { data: ref } = await dancer.rest.git.getRef({ + owner: gh.owner, + repo: gh.repo, + ref: `heads/${defaultbranch}`, + }); + const sha = ref.object.sha; + + await dancer.rest.git.createRef({ + owner: gh.owner, + repo: gh.repo, + ref: `refs/heads/${branch}`, + sha, + }); + + let config: Partial = {}; + let filesha: string | undefined; + + try { + const { data } = await dancer.rest.repos.getContent({ + owner: gh.owner, + repo: gh.repo, + path: '.github/tigent.yml', + ref: defaultbranch, + }); + if ('content' in data) { + const content = Buffer.from(data.content, 'base64').toString(); + config = (parse(content) as Partial) || {}; + filesha = data.sha; + } + } catch {} + + if (!config.examples) config.examples = []; + config.examples.push({ title, labels }); + + const yaml = stringify(config); + + await dancer.rest.repos.createOrUpdateFileContents({ + owner: gh.owner, + repo: gh.repo, + path: '.github/tigent.yml', + message: `fix: add learning example from #${issue}`, + content: Buffer.from(yaml).toString('base64'), + branch, + ...(filesha ? { sha: filesha } : {}), + }); + + await dancer.rest.pulls.create({ + owner: gh.owner, + repo: gh.repo, + title: `fix: learn from #${issue} correction`, + body: `adds example to \`.github/tigent.yml\` from issue #${issue} correction.\n\n\`\`\`yaml\n- title: "${title}"\n labels: [${labels.join(', ')}]\n\`\`\``, + head: branch, + base: defaultbranch, + }); +} diff --git a/app/api/webhook/route.ts b/app/api/webhook/route.ts index d0be39e..b599934 100644 --- a/app/api/webhook/route.ts +++ b/app/api/webhook/route.ts @@ -1,196 +1,47 @@ -import { App } from "octokit" -import { generateObject } from "ai" -import { z } from "zod" -import { parse } from "yaml" -import type { Octokit } from "octokit" +import { App } from 'octokit'; +import { getconfig } from './triage'; +import { triageissue, triagepr } from './triage'; +import { handlecomment } from './feedback'; const app = new App({ - appId: process.env.GITHUB_APP_ID!, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY!.replace(/\\n/g, "\n"), - webhooks: { secret: process.env.GITHUB_WEBHOOK_SECRET! }, -}) - -app.webhooks.on("issues.opened", async ({ octokit, payload }) => { - const owner = payload.repository.owner.login - const repo = payload.repository.name - const gh = { octokit, owner, repo } - const config = await getconfig(gh) - await triageissue(gh, config, payload.issue.number) -}) - -app.webhooks.on("pull_request.opened", async ({ octokit, payload }) => { - const owner = payload.repository.owner.login - const repo = payload.repository.name - const gh = { octokit, owner, repo } - const config = await getconfig(gh) - await triagepr(gh, config, payload.pull_request.number) -}) + appId: process.env.GITHUB_APP_ID!, + privateKey: process.env.GITHUB_APP_PRIVATE_KEY!.replace(/\\n/g, '\n'), + webhooks: { secret: process.env.GITHUB_WEBHOOK_SECRET! }, +}); + +app.webhooks.on('issues.opened', async ({ octokit, payload }) => { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const gh = { octokit, owner, repo }; + const config = await getconfig(gh); + await triageissue(gh, config, payload.issue.number); +}); + +app.webhooks.on('pull_request.opened', async ({ octokit, payload }) => { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const gh = { octokit, owner, repo }; + const config = await getconfig(gh); + await triagepr(gh, config, payload.pull_request.number); +}); + +app.webhooks.on('issue_comment.created', async ({ octokit, payload }) => { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const gh = { octokit, owner, repo }; + const config = await getconfig(gh); + await handlecomment(gh, config, payload); +}); export async function POST(req: Request) { - const body = await req.text() - try { - await app.webhooks.verifyAndReceive({ - id: req.headers.get("x-github-delivery") || "", - name: req.headers.get("x-github-event") as any, - payload: body, - signature: req.headers.get("x-hub-signature-256") || "", - }) - } catch {} - return new Response("ok") -} - -interface Gh { - octokit: Octokit - owner: string - repo: string -} - -interface Config { - confidence: number - model: string -} - -const defaultconfig: Config = { - confidence: 0.6, - model: "openai/gpt-5-nano", -} - -async function getconfig(gh: Gh): Promise { - try { - const { data } = await gh.octokit.rest.repos.getContent({ - owner: gh.owner, - repo: gh.repo, - path: ".github/tigent.yml", - mediaType: { format: "raw" }, - }) - const yaml = data as unknown as string - const parsed = parse(yaml) as Partial - return { ...defaultconfig, ...parsed } - } catch { - return defaultconfig - } -} - -interface Label { - name: string - description: string -} - -async function fetchlabels(gh: Gh): Promise { - const { data } = await gh.octokit.rest.issues.listLabelsForRepo({ - owner: gh.owner, - repo: gh.repo, - per_page: 100, - }) - return data.map((l) => ({ name: l.name, description: l.description || "" })) -} - -async function addlabels(gh: Gh, issue: number, labels: string[]) { - if (labels.length === 0) return - await gh.octokit.rest.issues.addLabels({ - owner: gh.owner, - repo: gh.repo, - issue_number: issue, - labels, - }) -} - -async function react(gh: Gh, issue: number) { - await gh.octokit.rest.reactions.createForIssue({ - owner: gh.owner, - repo: gh.repo, - issue_number: issue, - content: "eyes", - }) -} - -const schema = z.object({ - labels: z.array(z.string()), - confidence: z.number().min(0).max(1), - reasoning: z.string(), -}) - -async function triageissue(gh: Gh, config: Config, number: number) { - await react(gh, number) - - const [issue, labels] = await Promise.all([ - gh.octokit.rest.issues.get({ owner: gh.owner, repo: gh.repo, issue_number: number }), - fetchlabels(gh), - ]) - - const labellist = labels - .map((l) => (l.description ? `- ${l.name}: ${l.description}` : `- ${l.name}`)) - .join("\n") - - const { object } = await generateObject({ - model: config.model, - schema, - system: `you are a github issue classifier. assign labels based on the issue content. - -available labels: -${labellist} - -rules: -- only use labels from the list above -- pick labels that match the issue content -- use label descriptions to understand what each label means -- be conservative, only add labels you are confident about`, - prompt: `title: ${issue.data.title} - -body: -${issue.data.body || "no description"}`, - }) - - const valid = object.labels.filter((l) => labels.some((x) => x.name === l)) - if (object.confidence >= config.confidence && valid.length > 0) { - await addlabels(gh, number, valid) - } -} - -async function triagepr(gh: Gh, config: Config, number: number) { - await react(gh, number) - - const [pr, files, labels] = await Promise.all([ - gh.octokit.rest.pulls.get({ owner: gh.owner, repo: gh.repo, pull_number: number }), - gh.octokit.rest.pulls.listFiles({ - owner: gh.owner, - repo: gh.repo, - pull_number: number, - per_page: 100, - }), - fetchlabels(gh), - ]) - - const labellist = labels - .map((l) => (l.description ? `- ${l.name}: ${l.description}` : `- ${l.name}`)) - .join("\n") - - const filelist = files.data.map((f) => f.filename).join("\n") - - const { object } = await generateObject({ - model: config.model, - schema, - system: `you are a github pull request classifier. assign labels based on the pr content. - -available labels: -${labellist} - -rules: -- only use labels from the list above -- pick labels that match the pr content and changed files -- use label descriptions to understand what each label means -- be conservative, only add labels you are confident about`, - prompt: `title: ${pr.data.title} - -body: -${pr.data.body || "no description"} - -changed files: -${filelist}`, - }) - - const valid = object.labels.filter((l) => labels.some((x) => x.name === l)) - if (object.confidence >= config.confidence && valid.length > 0) { - await addlabels(gh, number, valid) - } + const body = await req.text(); + try { + await app.webhooks.verifyAndReceive({ + id: req.headers.get('x-github-delivery') || '', + name: req.headers.get('x-github-event') as any, + payload: body, + signature: req.headers.get('x-hub-signature-256') || '', + }); + } catch {} + return new Response('ok'); } diff --git a/app/api/webhook/triage.ts b/app/api/webhook/triage.ts new file mode 100644 index 0000000..3302b63 --- /dev/null +++ b/app/api/webhook/triage.ts @@ -0,0 +1,189 @@ +import { generateObject } from 'ai'; +import { z } from 'zod'; +import { parse } from 'yaml'; +import type { Octokit } from 'octokit'; + +export interface Gh { + octokit: Octokit; + owner: string; + repo: string; +} + +export interface Example { + title: string; + labels: string[]; +} + +export interface Config { + confidence: number; + model: string; + examples: Example[]; +} + +export interface Label { + name: string; + description: string; +} + +export const defaultconfig: Config = { + confidence: 0.6, + model: 'openai/gpt-5-nano', + examples: [], +}; + +export async function getconfig(gh: Gh): Promise { + try { + const { data } = await gh.octokit.rest.repos.getContent({ + owner: gh.owner, + repo: gh.repo, + path: '.github/tigent.yml', + mediaType: { format: 'raw' }, + }); + const yaml = data as unknown as string; + const parsed = parse(yaml) as Partial; + return { ...defaultconfig, ...parsed }; + } catch { + return defaultconfig; + } +} + +export async function fetchlabels(gh: Gh): Promise { + const { data } = await gh.octokit.rest.issues.listLabelsForRepo({ + owner: gh.owner, + repo: gh.repo, + per_page: 100, + }); + return data.map(l => ({ name: l.name, description: l.description || '' })); +} + +export async function addlabels(gh: Gh, issue: number, labels: string[]) { + if (labels.length === 0) return; + await gh.octokit.rest.issues.addLabels({ + owner: gh.owner, + repo: gh.repo, + issue_number: issue, + labels, + }); +} + +export async function react(gh: Gh, issue: number) { + await gh.octokit.rest.reactions.createForIssue({ + owner: gh.owner, + repo: gh.repo, + issue_number: issue, + content: 'eyes', + }); +} + +export const schema = z.object({ + labels: z.array(z.string()), + confidence: z.number().min(0).max(1), + reasoning: z.string(), +}); + +function examplesblock(examples: Example[]): string { + if (examples.length === 0) return ''; + const lines = examples.map(e => `- "${e.title}" → ${e.labels.join(', ')}`); + return `\nexamples from past corrections:\n${lines.join('\n')}\n`; +} + +export async function classify( + config: Config, + labels: Label[], + title: string, + body: string, + extra?: string, +) { + const labellist = labels + .map(l => (l.description ? `- ${l.name}: ${l.description}` : `- ${l.name}`)) + .join('\n'); + + const examples = examplesblock(config.examples); + + const system = `you are a github issue classifier. assign labels based on the content. + +available labels: +${labellist} +${examples} +rules: +- only use labels from the list above +- pick labels that match the content +- use label descriptions to understand what each label means +- be conservative, only add labels you are confident about`; + + const prompt = `title: ${title} + +body: +${body || 'no description'}${extra ? `\n\n${extra}` : ''}`; + + const { object } = await generateObject({ + model: config.model, + schema, + system, + prompt, + }); + const valid = object.labels.filter(l => labels.some(x => x.name === l)); + return { + labels: valid, + confidence: object.confidence, + reasoning: object.reasoning, + }; +} + +export async function triageissue(gh: Gh, config: Config, number: number) { + await react(gh, number); + + const [issue, labels] = await Promise.all([ + gh.octokit.rest.issues.get({ + owner: gh.owner, + repo: gh.repo, + issue_number: number, + }), + fetchlabels(gh), + ]); + + const result = await classify( + config, + labels, + issue.data.title, + issue.data.body || '', + ); + + if (result.confidence >= config.confidence && result.labels.length > 0) { + await addlabels(gh, number, result.labels); + } +} + +export async function triagepr(gh: Gh, config: Config, number: number) { + await react(gh, number); + + const [pr, files, labels] = await Promise.all([ + gh.octokit.rest.pulls.get({ + owner: gh.owner, + repo: gh.repo, + pull_number: number, + }), + gh.octokit.rest.pulls.listFiles({ + owner: gh.owner, + repo: gh.repo, + pull_number: number, + per_page: 100, + }), + fetchlabels(gh), + ]); + + const filelist = files.data.map(f => f.filename).join('\n'); + const extra = `changed files:\n${filelist}`; + + const result = await classify( + config, + labels, + pr.data.title, + pr.data.body || '', + extra, + ); + + if (result.confidence >= config.confidence && result.labels.length > 0) { + await addlabels(gh, number, result.labels); + } +} diff --git a/app/docs/config.ts b/app/docs/config.ts index 721b524..1b33fcd 100644 --- a/app/docs/config.ts +++ b/app/docs/config.ts @@ -14,6 +14,7 @@ export const navigation = [ items: [ { title: 'Configuration', href: '/docs/config' }, { title: 'Labels', href: '/docs/labels' }, + { title: 'Feedback', href: '/docs/feedback' }, ], }, ]; diff --git a/app/docs/config/page.tsx b/app/docs/config/page.tsx index 47f0f21..a4a5bad 100644 --- a/app/docs/config/page.tsx +++ b/app/docs/config/page.tsx @@ -48,17 +48,43 @@ export default function Config() { model

- AI model to use for classification. Default: - openai/gpt-5-nano + AI model to use for classification. Default: openai/gpt-5-nano

model: openai/gpt-5-nano -
-

A minimal config file:

- {`confidence: 0.7`} +
+
+
+

+ examples +

+

+ Few-shot examples from past corrections. These are injected into + the AI prompt to improve future classifications. Managed + automatically by the feedback loop. +

+ {`examples: + - title: "app crashes on startup" + labels: [bug, p1] + - title: "add dark mode support" + labels: [feature]`} +
+
+
+ +
+

A complete config file:

+ {`confidence: 0.7 + +examples: + - title: "app crashes on startup" + labels: [bug, p1]`}
diff --git a/app/docs/feedback/page.tsx b/app/docs/feedback/page.tsx new file mode 100644 index 0000000..8294964 --- /dev/null +++ b/app/docs/feedback/page.tsx @@ -0,0 +1,70 @@ +import type { Metadata } from 'next'; +import { Header, Section, Code, Codeinline, Prevnext } from '../components'; + +export const metadata: Metadata = { + title: 'Feedback', + description: 'Teach Tigent by correcting its labeling decisions.', +}; + +export default function Feedback() { + return ( +
+
+ +
+

+ Comment on any issue or PR to interact with Tigent. Only repository + owners, members, and collaborators can use these commands. +

+ +
+
+

+ @tigent why +

+

+ Re-classifies the issue and posts the reasoning as a comment. +

+ @tigent why +
+ +
+

+ @tigent wrong +

+

+ Removes the AI-assigned labels and applies the correct ones. + Creates a PR to save the correction as a learning example. +

+ @tigent wrong, should be bug, p1 +
+
+
+ +
+

+ When you use the wrong command, Tigent creates a PR that adds the + correction to your config file as an example. Future classifications + use these examples to make better decisions. +

+ {`confidence: 0.7 +examples: + - title: "app crashes on startup" + labels: [bug, p1]`} +
+ +
+

+ Only users with OWNER, MEMBER, or COLLABORATOR association can use + feedback commands. Comments from other users are ignored. +

+
+ + +
+ ); +} diff --git a/app/globals.css b/app/globals.css index 67f740b..a785b29 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @theme { --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; diff --git a/biome.json b/biome.json index e0f786f..1037d77 100644 --- a/biome.json +++ b/biome.json @@ -1,21 +1,21 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json", - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "css": { - "parser": { - "cssModules": true - } - }, - "files": { - "ignore": ["*.css"] - } + "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "css": { + "parser": { + "cssModules": true + } + }, + "files": { + "ignore": ["*.css"] + } } diff --git a/package.json b/package.json index 0c8a4f6..fba890c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start": "next start", "lint": "biome lint .", "format": "biome format --write .", - "prettier-fix": "prettier --write \"**/*.{js,ts,tsx,md,mdx,svelte}\"" + "prettier-fix": "prettier --write \"**/*.{js,ts,tsx,md,mdx,svelte}\"", + "prepare": "husky" }, "lint-staged": { "*": [ @@ -19,6 +20,7 @@ "@biomejs/biome": "^2.3.12", "@types/node": "^25.0.10", "@types/react": "^19.2.9", + "husky": "^9.1.7", "prettier": "^3.5.3" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdac403..d0d2d04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@types/react': specifier: ^19.2.9 version: 19.2.9 + husky: + specifier: ^9.1.7 + version: 9.1.7 prettier: specifier: ^3.5.3 version: 3.8.1 @@ -635,6 +638,11 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1317,6 +1325,8 @@ snapshots: graceful-fs@4.2.11: {} + husky@9.1.7: {} + jiti@2.6.1: {} json-schema@0.4.0: {} diff --git a/postcss.config.mjs b/postcss.config.mjs index e627111..a34a3d5 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,5 @@ export default { plugins: { - "@tailwindcss/postcss": {} - } -} + '@tailwindcss/postcss': {}, + }, +}; diff --git a/tsconfig.json b/tsconfig.json index c9c3943..9dce394 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,7 @@ "@/*": ["./*"] }, // Environment setup & latest features - "lib": [ - "ESNext", - "DOM", - "DOM.Iterable" - ], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", @@ -46,7 +42,5 @@ "**/*.ts", "**/*.tsx" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] }