feat: Screwdriver v1.0.0
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.venv/
|
||||||
|
.verdict
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#0e639c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0e639c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#0e639c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0e639c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||||
|
After Width: | Height: | Size: 347 B |
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Screwdriver — Non-Conversational AI Tools",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Dedicated, non-conversational AI interfaces for developers. Screwdrivers, not Swiss Army knives.",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
|
"scripting",
|
||||||
|
"contextMenus",
|
||||||
|
"clipboardWrite"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "src/popup/popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.svg",
|
||||||
|
"32": "icons/icon32.svg",
|
||||||
|
"48": "icons/icon48.svg",
|
||||||
|
"128": "icons/icon128.svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "src/background.ts",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["src/content/content.ts"],
|
||||||
|
"css": ["src/content/content.css"],
|
||||||
|
"run_at": "document_end"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options_page": "src/options/options.html",
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.svg",
|
||||||
|
"32": "icons/icon32.svg",
|
||||||
|
"48": "icons/icon48.svg",
|
||||||
|
"128": "icons/icon128.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2277
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "non-conversational-ai-tool-interface",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@webcomponents/custom-elements": "^1.6.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@crxjs/vite-plugin": "^2.0.0-beta.28",
|
||||||
|
"@types/chrome": "^0.0.268",
|
||||||
|
"@types/node": "^20.11.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.12",
|
||||||
|
"vitest": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { transformText, type TransformType } from './utils/text-transformer.js'
|
||||||
|
import { getSettings } from './utils/storage.js'
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
|
setupContextMenus()
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(() => {
|
||||||
|
setupContextMenus()
|
||||||
|
})
|
||||||
|
|
||||||
|
function setupContextMenus() {
|
||||||
|
chrome.contextMenus.removeAll(() => {
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: 'screwdriver_root',
|
||||||
|
title: 'Screwdriver',
|
||||||
|
contexts: ['selection'],
|
||||||
|
})
|
||||||
|
|
||||||
|
const quickTransforms: { type: TransformType; title: string }[] = [
|
||||||
|
{ type: 'base64encode', title: 'Base64 Encode' },
|
||||||
|
{ type: 'base64decode', title: 'Base64 Decode' },
|
||||||
|
{ type: 'urlescape', title: 'URL Encode' },
|
||||||
|
{ type: 'urlunescape', title: 'URL Decode' },
|
||||||
|
{ type: 'jsonpretty', title: 'Format JSON' },
|
||||||
|
{ type: 'jsoncompact', title: 'Minify JSON' },
|
||||||
|
{ type: 'camelcase', title: 'camelCase' },
|
||||||
|
{ type: 'snakecase', title: 'snake_case' },
|
||||||
|
{ type: 'kebabcase', title: 'kebab-case' },
|
||||||
|
{ type: 'uppercase', title: 'UPPERCASE' },
|
||||||
|
{ type: 'lowercase', title: 'lowercase' },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const t of quickTransforms) {
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: `transform_${t.type}`,
|
||||||
|
parentId: 'screwdriver_root',
|
||||||
|
title: t.title,
|
||||||
|
contexts: ['selection'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: 'open_popup',
|
||||||
|
parentId: 'screwdriver_root',
|
||||||
|
title: 'Open Screwdriver Panel',
|
||||||
|
contexts: ['selection'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
|
if (!tab?.id) return
|
||||||
|
|
||||||
|
if (info.menuItemId === 'open_popup') {
|
||||||
|
chrome.action.openPopup()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof info.menuItemId === 'string' && info.menuItemId.startsWith('transform_')) {
|
||||||
|
const type = info.menuItemId.replace('transform_', '') as TransformType
|
||||||
|
const selectedText = info.selectionText ?? ''
|
||||||
|
const result = transformText(selectedText, type)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
func: (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text).catch(() => {
|
||||||
|
// Fallback for pages without clipboard permission
|
||||||
|
const el = document.createElement('textarea')
|
||||||
|
el.value = text
|
||||||
|
document.body.appendChild(el)
|
||||||
|
el.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(el)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
args: [result.result],
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.notifications.create({
|
||||||
|
type: 'basic',
|
||||||
|
iconUrl: 'icons/icon128.png',
|
||||||
|
title: 'Screwdriver',
|
||||||
|
message: `${type} result copied to clipboard`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||||
|
if (request.action === 'getSettings') {
|
||||||
|
getSettings().then(sendResponse).catch(() => sendResponse(null))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'transform') {
|
||||||
|
const result = transformText(request.text, request.type as TransformType)
|
||||||
|
sendResponse(result)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.action === 'ping') {
|
||||||
|
sendResponse({ ok: true })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/* Screwdriver content script styles */
|
||||||
|
#screwdriver-inline-panel textarea::placeholder {
|
||||||
|
color: #6e6e6e;
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { transformText, TRANSFORM_OPTIONS, type TransformType } from '../utils/text-transformer.js'
|
||||||
|
|
||||||
|
let panel: HTMLElement | null = null
|
||||||
|
|
||||||
|
function createInlinePanel() {
|
||||||
|
if (panel) return
|
||||||
|
|
||||||
|
panel = document.createElement('div')
|
||||||
|
panel.id = 'screwdriver-inline-panel'
|
||||||
|
panel.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
width: 320px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||||
|
z-index: 2147483647;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const header = document.createElement('div')
|
||||||
|
header.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
background: #252526;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cccccc;
|
||||||
|
`
|
||||||
|
header.textContent = 'Screwdriver'
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button')
|
||||||
|
closeBtn.textContent = '×'
|
||||||
|
closeBtn.style.cssText = `
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 4px;
|
||||||
|
`
|
||||||
|
closeBtn.onclick = () => {
|
||||||
|
if (panel) {
|
||||||
|
panel.remove()
|
||||||
|
panel = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header.appendChild(closeBtn)
|
||||||
|
panel.appendChild(header)
|
||||||
|
|
||||||
|
const content = document.createElement('div')
|
||||||
|
content.style.cssText = 'padding: 12px; overflow-y: auto;'
|
||||||
|
|
||||||
|
const input = document.createElement('textarea')
|
||||||
|
input.placeholder = 'Paste text here...'
|
||||||
|
input.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`
|
||||||
|
content.appendChild(input)
|
||||||
|
|
||||||
|
const select = document.createElement('select')
|
||||||
|
select.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`
|
||||||
|
for (const opt of TRANSFORM_OPTIONS) {
|
||||||
|
const option = document.createElement('option')
|
||||||
|
option.value = opt.type
|
||||||
|
option.textContent = `${opt.label} — ${opt.description}`
|
||||||
|
select.appendChild(option)
|
||||||
|
}
|
||||||
|
content.appendChild(select)
|
||||||
|
|
||||||
|
const output = document.createElement('textarea')
|
||||||
|
output.readOnly = true
|
||||||
|
output.placeholder = 'Result...'
|
||||||
|
output.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #9cdcfe;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
resize: vertical;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`
|
||||||
|
content.appendChild(output)
|
||||||
|
|
||||||
|
const btnRow = document.createElement('div')
|
||||||
|
btnRow.style.cssText = 'display: flex; gap: 8px;'
|
||||||
|
|
||||||
|
const transformBtn = document.createElement('button')
|
||||||
|
transformBtn.textContent = 'Transform'
|
||||||
|
transformBtn.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: #0e639c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
transformBtn.onmouseenter = () => { transformBtn.style.background = '#1177bb' }
|
||||||
|
transformBtn.onmouseleave = () => { transformBtn.style.background = '#0e639c' }
|
||||||
|
transformBtn.onclick = () => {
|
||||||
|
const result = transformText(input.value, select.value as TransformType)
|
||||||
|
if (result.success) {
|
||||||
|
output.value = result.result
|
||||||
|
output.style.borderColor = '#3c3c3c'
|
||||||
|
} else {
|
||||||
|
output.value = `Error: ${result.error ?? 'Unknown error'}`
|
||||||
|
output.style.borderColor = '#f44336'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btnRow.appendChild(transformBtn)
|
||||||
|
|
||||||
|
const copyBtn = document.createElement('button')
|
||||||
|
copyBtn.textContent = 'Copy'
|
||||||
|
copyBtn.style.cssText = `
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
copyBtn.onclick = () => {
|
||||||
|
navigator.clipboard.writeText(output.value)
|
||||||
|
copyBtn.textContent = 'Copied!'
|
||||||
|
setTimeout(() => { copyBtn.textContent = 'Copy' }, 1500)
|
||||||
|
}
|
||||||
|
btnRow.appendChild(copyBtn)
|
||||||
|
|
||||||
|
content.appendChild(btnRow)
|
||||||
|
panel.appendChild(content)
|
||||||
|
|
||||||
|
document.body.appendChild(panel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcut: Alt+Shift+S to toggle panel
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.altKey && e.shiftKey && e.key === 'S') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (panel) {
|
||||||
|
panel.remove()
|
||||||
|
panel = null
|
||||||
|
} else {
|
||||||
|
createInlinePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listen for messages from popup/background
|
||||||
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||||
|
if (request.action === 'showInlinePanel') {
|
||||||
|
createInlinePanel()
|
||||||
|
sendResponse({ ok: true })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (request.action === 'getSelectedText') {
|
||||||
|
sendResponse({ text: window.getSelection()?.toString() ?? '' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #d4d4d4;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
color: #858585;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #cccccc;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting span {
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: #0e639c;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting select {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting select:focus {
|
||||||
|
border-color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #f4433620;
|
||||||
|
border: 1px solid #f44336;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #f4433640;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #0e639c;
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Screwdriver Settings</title>
|
||||||
|
<link rel="stylesheet" href="options.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>⚙ Screwdriver Settings</h1>
|
||||||
|
<p>Non-conversational AI tools for developers</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="settings">
|
||||||
|
<section class="setting-group">
|
||||||
|
<h2>General</h2>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Show danger warnings for destructive commands</span>
|
||||||
|
<input type="checkbox" id="show-danger" checked />
|
||||||
|
</label>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Copy command to clipboard on translate</span>
|
||||||
|
<input type="checkbox" id="copy-on-translate" checked />
|
||||||
|
</label>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Enable context menu (right-click transforms)</span>
|
||||||
|
<input type="checkbox" id="context-menu" checked />
|
||||||
|
</label>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Enable inline panel (Alt+Shift+S)</span>
|
||||||
|
<input type="checkbox" id="inline-panel" checked />
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="setting-group">
|
||||||
|
<h2>Shell Preferences</h2>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Preferred shell</span>
|
||||||
|
<select id="preferred-shell">
|
||||||
|
<option value="bash">Bash</option>
|
||||||
|
<option value="zsh">Zsh</option>
|
||||||
|
<option value="fish">Fish</option>
|
||||||
|
<option value="powershell">PowerShell</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="setting-group">
|
||||||
|
<h2>Appearance</h2>
|
||||||
|
<label class="setting">
|
||||||
|
<span>Theme</span>
|
||||||
|
<select id="theme">
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="setting-group">
|
||||||
|
<h2>Data</h2>
|
||||||
|
<div class="setting">
|
||||||
|
<span>Clipboard history</span>
|
||||||
|
<button id="clear-history" class="btn-secondary">Clear History</button>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<span>Reset all settings</span>
|
||||||
|
<button id="reset-settings" class="btn-danger">Reset</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="status" class="status hidden">Settings saved</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="options.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { getSettings, setSettings, clearClipboardHistory } from '../utils/storage.js'
|
||||||
|
|
||||||
|
const showDanger = document.getElementById('show-danger') as HTMLInputElement
|
||||||
|
const copyOnTranslate = document.getElementById('copy-on-translate') as HTMLInputElement
|
||||||
|
const contextMenu = document.getElementById('context-menu') as HTMLInputElement
|
||||||
|
const inlinePanel = document.getElementById('inline-panel') as HTMLInputElement
|
||||||
|
const preferredShell = document.getElementById('preferred-shell') as HTMLSelectElement
|
||||||
|
const theme = document.getElementById('theme') as HTMLSelectElement
|
||||||
|
const statusEl = document.getElementById('status') as HTMLElement
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
const settings = await getSettings()
|
||||||
|
showDanger.checked = settings.showDangerWarning
|
||||||
|
copyOnTranslate.checked = settings.copyOnTranslate
|
||||||
|
contextMenu.checked = settings.contextMenuEnabled
|
||||||
|
inlinePanel.checked = settings.inlinePanelEnabled
|
||||||
|
preferredShell.value = settings.preferredShell
|
||||||
|
theme.value = settings.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
await setSettings({
|
||||||
|
showDangerWarning: showDanger.checked,
|
||||||
|
copyOnTranslate: copyOnTranslate.checked,
|
||||||
|
contextMenuEnabled: contextMenu.checked,
|
||||||
|
inlinePanelEnabled: inlinePanel.checked,
|
||||||
|
preferredShell: preferredShell.value as 'bash' | 'zsh' | 'fish' | 'powershell',
|
||||||
|
theme: theme.value as 'light' | 'dark' | 'auto',
|
||||||
|
})
|
||||||
|
statusEl.classList.remove('hidden')
|
||||||
|
setTimeout(() => statusEl.classList.add('hidden'), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
showDanger.addEventListener('change', save)
|
||||||
|
copyOnTranslate.addEventListener('change', save)
|
||||||
|
contextMenu.addEventListener('change', save)
|
||||||
|
inlinePanel.addEventListener('change', save)
|
||||||
|
preferredShell.addEventListener('change', save)
|
||||||
|
theme.addEventListener('change', save)
|
||||||
|
|
||||||
|
document.getElementById('clear-history')?.addEventListener('click', async () => {
|
||||||
|
await clearClipboardHistory()
|
||||||
|
statusEl.textContent = 'History cleared'
|
||||||
|
statusEl.classList.remove('hidden')
|
||||||
|
setTimeout(() => {
|
||||||
|
statusEl.classList.add('hidden')
|
||||||
|
statusEl.textContent = 'Settings saved'
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('reset-settings')?.addEventListener('click', async () => {
|
||||||
|
if (!confirm('Reset all settings to defaults?')) return
|
||||||
|
await chrome.storage.sync.remove('screwdriver_settings')
|
||||||
|
await loadSettings()
|
||||||
|
statusEl.textContent = 'Settings reset'
|
||||||
|
statusEl.classList.remove('hidden')
|
||||||
|
setTimeout(() => {
|
||||||
|
statusEl.classList.add('hidden')
|
||||||
|
statusEl.textContent = 'Settings saved'
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #d4d4d4;
|
||||||
|
background: #1e1e1e;
|
||||||
|
width: 380px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo svg {
|
||||||
|
color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions button:hover {
|
||||||
|
background: #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #858585;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #0e639c;
|
||||||
|
border-bottom-color: #0e639c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: #858585;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #d4d4d4;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
select:focus,
|
||||||
|
textarea:focus {
|
||||||
|
border-color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result code {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9cdcfe;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #0e639c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #1177bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #858585;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
color: #cccccc;
|
||||||
|
border-color: #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.danger {
|
||||||
|
background: #f4433620;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.success {
|
||||||
|
background: #4caf5020;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #6e6e6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .hint {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-chip {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #0e639c;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-chip:hover {
|
||||||
|
border-color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-btn {
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-family: 'SF Mono', Monaco, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-btn:hover {
|
||||||
|
border-color: #0e639c;
|
||||||
|
color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cccccc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag input {
|
||||||
|
accent-color: #0e639c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regex-result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #252526;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid #3c3c3c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-index {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #858585;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-text {
|
||||||
|
color: #9cdcfe;
|
||||||
|
font-family: monospace;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-groups {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-error {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-empty {
|
||||||
|
color: #6e6e6e;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-left: 3px solid #0e639c;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-top: 1px solid #3c3c3c;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6e6e6e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #3c3c3c;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4c4c4c;
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Screwdriver</title>
|
||||||
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
||||||
|
</svg>
|
||||||
|
Screwdriver
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="btn-theme" title="Toggle theme">◐</button>
|
||||||
|
<button id="btn-settings" title="Settings">⚙</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="tabs">
|
||||||
|
<button class="tab active" data-tab="shell">Shell</button>
|
||||||
|
<button class="tab" data-tab="transform">Transform</button>
|
||||||
|
<button class="tab" data-tab="git">Git</button>
|
||||||
|
<button class="tab" data-tab="regex">Regex</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Shell Tab -->
|
||||||
|
<section id="tab-shell" class="tab-content active">
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Describe what you want</label>
|
||||||
|
<input id="shell-input" type="text" placeholder="e.g. list all files, git status, docker ps" autocomplete="off" />
|
||||||
|
<div id="shell-suggestions" class="suggestions"></div>
|
||||||
|
<div id="shell-result" class="result hidden">
|
||||||
|
<div class="result-header">
|
||||||
|
<span id="shell-desc"></span>
|
||||||
|
<span id="shell-danger" class="badge danger hidden">DANGER</span>
|
||||||
|
</div>
|
||||||
|
<code id="shell-command"></code>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button id="shell-copy" class="btn-primary">Copy</button>
|
||||||
|
<button id="shell-explain" class="btn-secondary">Explain</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="shell-empty" class="empty-state">
|
||||||
|
<p>Type a command description above</p>
|
||||||
|
<p class="hint">Try: "list files", "git log", "docker ps", "find file"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Quick Commands</label>
|
||||||
|
<div id="quick-commands" class="quick-grid"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Transform Tab -->
|
||||||
|
<section id="tab-transform" class="tab-content">
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Input</label>
|
||||||
|
<textarea id="transform-input" placeholder="Paste text here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Transform</label>
|
||||||
|
<select id="transform-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Result</label>
|
||||||
|
<textarea id="transform-output" readonly placeholder="Result..."></textarea>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button id="transform-run" class="btn-primary">Transform</button>
|
||||||
|
<button id="transform-copy" class="btn-secondary">Copy</button>
|
||||||
|
<button id="transform-clear" class="btn-ghost">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Git Tab -->
|
||||||
|
<section id="tab-git" class="tab-content">
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Paste conflict text</label>
|
||||||
|
<textarea id="git-input" placeholder="<<<<<<< HEAD
|
||||||
|
...your changes...
|
||||||
|
=======
|
||||||
|
...their changes...
|
||||||
|
>>>>>>> branch"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Strategy</label>
|
||||||
|
<select id="git-strategy">
|
||||||
|
<option value="">Auto-detect</option>
|
||||||
|
<option value="ours">Take ours</option>
|
||||||
|
<option value="theirs">Take theirs</option>
|
||||||
|
<option value="both">Merge both</option>
|
||||||
|
<option value="manual">Mark manual</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Resolution</label>
|
||||||
|
<textarea id="git-output" readonly placeholder="Resolved output..."></textarea>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button id="git-resolve" class="btn-primary">Resolve</button>
|
||||||
|
<button id="git-copy" class="btn-secondary">Copy</button>
|
||||||
|
<button id="git-clear" class="btn-ghost">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="git-explanation" class="explanation hidden"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Regex Tab -->
|
||||||
|
<section id="tab-regex" class="tab-content">
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Pattern</label>
|
||||||
|
<input id="regex-pattern" type="text" placeholder="e.g. \d+ or [a-z]+" />
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Flags</label>
|
||||||
|
<div class="flag-group">
|
||||||
|
<label class="flag"><input type="checkbox" id="flag-g" value="g" checked /> g (global)</label>
|
||||||
|
<label class="flag"><input type="checkbox" id="flag-i" value="i" /> i (ignore case)</label>
|
||||||
|
<label class="flag"><input type="checkbox" id="flag-m" value="m" /> m (multiline)</label>
|
||||||
|
<label class="flag"><input type="checkbox" id="flag-s" value="s" /> s (dotAll)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Test Text</label>
|
||||||
|
<textarea id="regex-text" placeholder="Text to test against..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="tool-section">
|
||||||
|
<label class="tool-label">Replacement (optional)</label>
|
||||||
|
<input id="regex-replace" type="text" placeholder="e.g. $1 or replacement text" />
|
||||||
|
</div>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button id="regex-test" class="btn-primary">Test</button>
|
||||||
|
<button id="regex-clear" class="btn-ghost">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="regex-result" class="regex-result hidden">
|
||||||
|
<div class="result-header">
|
||||||
|
<span id="regex-status"></span>
|
||||||
|
<span id="regex-count"></span>
|
||||||
|
</div>
|
||||||
|
<div id="regex-matches"></div>
|
||||||
|
<div id="regex-replace-preview" class="hidden">
|
||||||
|
<label>Replace preview:</label>
|
||||||
|
<code id="regex-replace-output"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<span>Alt+Shift+S on any page</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="popup.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
import { translateCommand } from '../utils/command-translator.js'
|
||||||
|
import { transformText, TRANSFORM_OPTIONS } from '../utils/text-transformer.js'
|
||||||
|
import { resolveConflicts } from '../utils/git-conflict-resolver.js'
|
||||||
|
import { testRegex } from '../utils/regex-tester.js'
|
||||||
|
import { addClipboardItem } from '../utils/storage.js'
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const target = tab.getAttribute('data-tab')
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'))
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'))
|
||||||
|
|
||||||
|
tab.classList.add('active')
|
||||||
|
document.getElementById(`tab-${target}`)?.classList.add('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Shell tab
|
||||||
|
const shellInput = document.getElementById('shell-input') as HTMLInputElement
|
||||||
|
const shellResult = document.getElementById('shell-result') as HTMLElement
|
||||||
|
const shellEmpty = document.getElementById('shell-empty') as HTMLElement
|
||||||
|
const shellDesc = document.getElementById('shell-desc') as HTMLElement
|
||||||
|
const shellDanger = document.getElementById('shell-danger') as HTMLElement
|
||||||
|
const shellCommand = document.getElementById('shell-command') as HTMLElement
|
||||||
|
const shellSuggestions = document.getElementById('shell-suggestions') as HTMLElement
|
||||||
|
|
||||||
|
shellInput.addEventListener('input', () => {
|
||||||
|
const result = translateCommand(shellInput.value)
|
||||||
|
shellSuggestions.innerHTML = ''
|
||||||
|
|
||||||
|
if (result.success && result.command) {
|
||||||
|
shellEmpty.classList.add('hidden')
|
||||||
|
shellResult.classList.remove('hidden')
|
||||||
|
shellDesc.textContent = result.description ?? ''
|
||||||
|
shellCommand.textContent = result.command
|
||||||
|
shellDanger.classList.toggle('hidden', !result.dangerous)
|
||||||
|
} else {
|
||||||
|
shellResult.classList.add('hidden')
|
||||||
|
shellEmpty.classList.remove('hidden')
|
||||||
|
|
||||||
|
if (result.suggestions) {
|
||||||
|
for (const s of result.suggestions) {
|
||||||
|
const chip = document.createElement('span')
|
||||||
|
chip.className = 'suggestion-chip'
|
||||||
|
chip.textContent = s
|
||||||
|
chip.addEventListener('click', () => {
|
||||||
|
shellInput.value = s
|
||||||
|
shellInput.dispatchEvent(new Event('input'))
|
||||||
|
})
|
||||||
|
shellSuggestions.appendChild(chip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('shell-copy')?.addEventListener('click', async () => {
|
||||||
|
const cmd = shellCommand.textContent ?? ''
|
||||||
|
await navigator.clipboard.writeText(cmd)
|
||||||
|
await addClipboardItem({ text: cmd, type: 'command' })
|
||||||
|
const btn = document.getElementById('shell-copy') as HTMLButtonElement
|
||||||
|
btn.textContent = 'Copied!'
|
||||||
|
setTimeout(() => { btn.textContent = 'Copy' }, 1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('shell-explain')?.addEventListener('click', () => {
|
||||||
|
const cmd = shellCommand.textContent ?? ''
|
||||||
|
const parts = cmd.split(' ')
|
||||||
|
const explanations: string[] = []
|
||||||
|
const cmdExplanations: Record<string, string> = {
|
||||||
|
'ls': 'List directory contents',
|
||||||
|
'cd': 'Change directory',
|
||||||
|
'pwd': 'Print working directory',
|
||||||
|
'cat': 'Display file contents',
|
||||||
|
'rm': 'Remove files/directories',
|
||||||
|
'cp': 'Copy files/directories',
|
||||||
|
'mv': 'Move/rename files',
|
||||||
|
'mkdir': 'Create directory',
|
||||||
|
'touch': 'Create empty file or update timestamp',
|
||||||
|
'grep': 'Search text using patterns',
|
||||||
|
'find': 'Search for files',
|
||||||
|
'ps': 'Report process status',
|
||||||
|
'kill': 'Send signal to process',
|
||||||
|
'df': 'Report disk space',
|
||||||
|
'du': 'Estimate file space usage',
|
||||||
|
'tar': 'Archive files',
|
||||||
|
'chmod': 'Change file permissions',
|
||||||
|
'chown': 'Change file owner',
|
||||||
|
'git': 'Version control system',
|
||||||
|
'docker': 'Container management',
|
||||||
|
'npm': 'Node package manager',
|
||||||
|
'curl': 'Transfer data from URL',
|
||||||
|
'ping': 'Test network connectivity',
|
||||||
|
'env': 'Show environment variables',
|
||||||
|
'whoami': 'Show current user',
|
||||||
|
'date': 'Show current date/time',
|
||||||
|
'lsof': 'List open files',
|
||||||
|
}
|
||||||
|
for (const part of parts) {
|
||||||
|
if (cmdExplanations[part]) {
|
||||||
|
explanations.push(`${part}: ${cmdExplanations[part]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (explanations.length > 0) {
|
||||||
|
alert(explanations.join('\n'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Quick commands
|
||||||
|
const quickGrid = document.getElementById('quick-commands') as HTMLElement
|
||||||
|
const quickCommands = [
|
||||||
|
'git status', 'git log', 'git add .', 'git commit',
|
||||||
|
'ls -la', 'pwd', 'docker ps', 'npm test',
|
||||||
|
]
|
||||||
|
for (const cmd of quickCommands) {
|
||||||
|
const btn = document.createElement('button')
|
||||||
|
btn.className = 'quick-btn'
|
||||||
|
btn.textContent = cmd
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
shellInput.value = cmd
|
||||||
|
shellInput.dispatchEvent(new Event('input'))
|
||||||
|
})
|
||||||
|
quickGrid.appendChild(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform tab
|
||||||
|
const transformInput = document.getElementById('transform-input') as HTMLTextAreaElement
|
||||||
|
const transformSelect = document.getElementById('transform-select') as HTMLSelectElement
|
||||||
|
const transformOutput = document.getElementById('transform-output') as HTMLTextAreaElement
|
||||||
|
|
||||||
|
for (const opt of TRANSFORM_OPTIONS) {
|
||||||
|
const option = document.createElement('option')
|
||||||
|
option.value = opt.type
|
||||||
|
option.textContent = `${opt.label} — ${opt.description}`
|
||||||
|
transformSelect.appendChild(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('transform-run')?.addEventListener('click', () => {
|
||||||
|
const result = transformText(transformInput.value, transformSelect.value as never)
|
||||||
|
if (result.success) {
|
||||||
|
transformOutput.value = result.result
|
||||||
|
transformOutput.style.borderColor = ''
|
||||||
|
} else {
|
||||||
|
transformOutput.value = `Error: ${result.error ?? 'Unknown'}`
|
||||||
|
transformOutput.style.borderColor = '#f44336'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('transform-copy')?.addEventListener('click', async () => {
|
||||||
|
await navigator.clipboard.writeText(transformOutput.value)
|
||||||
|
await addClipboardItem({ text: transformOutput.value, type: 'transform' })
|
||||||
|
const btn = document.getElementById('transform-copy') as HTMLButtonElement
|
||||||
|
btn.textContent = 'Copied!'
|
||||||
|
setTimeout(() => { btn.textContent = 'Copy' }, 1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('transform-clear')?.addEventListener('click', () => {
|
||||||
|
transformInput.value = ''
|
||||||
|
transformOutput.value = ''
|
||||||
|
transformOutput.style.borderColor = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Git tab
|
||||||
|
const gitInput = document.getElementById('git-input') as HTMLTextAreaElement
|
||||||
|
const gitStrategy = document.getElementById('git-strategy') as HTMLSelectElement
|
||||||
|
const gitOutput = document.getElementById('git-output') as HTMLTextAreaElement
|
||||||
|
const gitExplanation = document.getElementById('git-explanation') as HTMLElement
|
||||||
|
|
||||||
|
document.getElementById('git-resolve')?.addEventListener('click', () => {
|
||||||
|
const strategy = gitStrategy.value as 'ours' | 'theirs' | 'both' | 'manual' | ''
|
||||||
|
const result = resolveConflicts(gitInput.value, strategy || undefined)
|
||||||
|
|
||||||
|
if (result.success && result.resolution) {
|
||||||
|
gitOutput.value = result.resolution
|
||||||
|
gitOutput.style.borderColor = ''
|
||||||
|
gitExplanation.textContent = result.explanation
|
||||||
|
gitExplanation.classList.remove('hidden')
|
||||||
|
} else {
|
||||||
|
gitOutput.value = result.explanation
|
||||||
|
gitOutput.style.borderColor = '#f44336'
|
||||||
|
gitExplanation.classList.add('hidden')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('git-copy')?.addEventListener('click', async () => {
|
||||||
|
await navigator.clipboard.writeText(gitOutput.value)
|
||||||
|
await addClipboardItem({ text: gitOutput.value, type: 'conflict' })
|
||||||
|
const btn = document.getElementById('git-copy') as HTMLButtonElement
|
||||||
|
btn.textContent = 'Copied!'
|
||||||
|
setTimeout(() => { btn.textContent = 'Copy' }, 1500)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('git-clear')?.addEventListener('click', () => {
|
||||||
|
gitInput.value = ''
|
||||||
|
gitOutput.value = ''
|
||||||
|
gitExplanation.classList.add('hidden')
|
||||||
|
gitOutput.style.borderColor = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Regex tab
|
||||||
|
const regexPattern = document.getElementById('regex-pattern') as HTMLInputElement
|
||||||
|
const regexText = document.getElementById('regex-text') as HTMLTextAreaElement
|
||||||
|
const regexReplace = document.getElementById('regex-replace') as HTMLInputElement
|
||||||
|
const regexResult = document.getElementById('regex-result') as HTMLElement
|
||||||
|
const regexStatus = document.getElementById('regex-status') as HTMLElement
|
||||||
|
const regexCount = document.getElementById('regex-count') as HTMLElement
|
||||||
|
const regexMatches = document.getElementById('regex-matches') as HTMLElement
|
||||||
|
const regexReplacePreview = document.getElementById('regex-replace-preview') as HTMLElement
|
||||||
|
const regexReplaceOutput = document.getElementById('regex-replace-output') as HTMLElement
|
||||||
|
|
||||||
|
document.getElementById('regex-test')?.addEventListener('click', () => {
|
||||||
|
const flags = [
|
||||||
|
(document.getElementById('flag-g') as HTMLInputElement).checked ? 'g' : '',
|
||||||
|
(document.getElementById('flag-i') as HTMLInputElement).checked ? 'i' : '',
|
||||||
|
(document.getElementById('flag-m') as HTMLInputElement).checked ? 'm' : '',
|
||||||
|
(document.getElementById('flag-s') as HTMLInputElement).checked ? 's' : '',
|
||||||
|
].join('')
|
||||||
|
|
||||||
|
const result = testRegex({
|
||||||
|
pattern: regexPattern.value,
|
||||||
|
flags,
|
||||||
|
text: regexText.value,
|
||||||
|
replacement: regexReplace.value || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
regexResult.classList.remove('hidden')
|
||||||
|
|
||||||
|
if (!result.valid) {
|
||||||
|
regexStatus.textContent = 'Invalid regex'
|
||||||
|
regexStatus.className = 'badge danger'
|
||||||
|
regexCount.textContent = ''
|
||||||
|
regexMatches.innerHTML = `<div class="match-error">${result.error ?? ''}</div>`
|
||||||
|
regexReplacePreview.classList.add('hidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
regexStatus.textContent = 'Valid regex'
|
||||||
|
regexStatus.className = 'badge success'
|
||||||
|
regexCount.textContent = `${result.matches.length} match${result.matches.length !== 1 ? 'es' : ''}`
|
||||||
|
|
||||||
|
if (result.matches.length === 0) {
|
||||||
|
regexMatches.innerHTML = '<div class="match-empty">No matches found</div>'
|
||||||
|
} else {
|
||||||
|
regexMatches.innerHTML = result.matches.map((m, i) => `
|
||||||
|
<div class="match-item">
|
||||||
|
<span class="match-index">#${i + 1}</span>
|
||||||
|
<code class="match-text">${escapeHtml(m.full)}</code>
|
||||||
|
${m.groups.length > 0 ? `<span class="match-groups">Groups: ${m.groups.map((g, j) => `${j + 1}=${escapeHtml(g ?? '')}`).join(', ')}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.replacePreview !== undefined) {
|
||||||
|
regexReplaceOutput.textContent = result.replacePreview
|
||||||
|
regexReplacePreview.classList.remove('hidden')
|
||||||
|
} else {
|
||||||
|
regexReplacePreview.classList.add('hidden')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById('regex-clear')?.addEventListener('click', () => {
|
||||||
|
regexPattern.value = ''
|
||||||
|
regexText.value = ''
|
||||||
|
regexReplace.value = ''
|
||||||
|
regexResult.classList.add('hidden')
|
||||||
|
;(document.getElementById('flag-g') as HTMLInputElement).checked = true
|
||||||
|
;(document.getElementById('flag-i') as HTMLInputElement).checked = false
|
||||||
|
;(document.getElementById('flag-m') as HTMLInputElement).checked = false
|
||||||
|
;(document.getElementById('flag-s') as HTMLInputElement).checked = false
|
||||||
|
})
|
||||||
|
|
||||||
|
function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
document.getElementById('btn-settings')?.addEventListener('click', () => {
|
||||||
|
chrome.runtime.openOptionsPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Theme toggle
|
||||||
|
document.getElementById('btn-theme')?.addEventListener('click', () => {
|
||||||
|
document.body.classList.toggle('dark')
|
||||||
|
})
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
export interface CommandPattern {
|
||||||
|
readonly patterns: readonly string[]
|
||||||
|
readonly command: string
|
||||||
|
readonly description: string
|
||||||
|
readonly dangerous: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMAND_PATTERNS: readonly CommandPattern[] = [
|
||||||
|
// Navigation
|
||||||
|
{
|
||||||
|
patterns: ['list files', 'show files', 'ls', 'what files', 'dir'],
|
||||||
|
command: 'ls -la',
|
||||||
|
description: 'List all files in current directory',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['list directory', 'show directory', 'current directory', 'pwd'],
|
||||||
|
command: 'pwd',
|
||||||
|
description: 'Print working directory',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['go up', 'parent directory', 'cd up', 'up one level'],
|
||||||
|
command: 'cd ..',
|
||||||
|
description: 'Go to parent directory',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['go home', 'home directory', 'cd home'],
|
||||||
|
command: 'cd ~',
|
||||||
|
description: 'Go to home directory',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// File operations
|
||||||
|
{
|
||||||
|
patterns: ['create file', 'new file', 'touch file', 'make file'],
|
||||||
|
command: 'touch filename',
|
||||||
|
description: 'Create empty file (replace "filename")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['create directory', 'new folder', 'mkdir', 'make directory'],
|
||||||
|
command: 'mkdir dirname',
|
||||||
|
description: 'Create directory (replace "dirname")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['remove file', 'delete file', 'rm file'],
|
||||||
|
command: 'rm filename',
|
||||||
|
description: 'Remove file (replace "filename") ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['remove directory', 'delete folder', 'rm dir', 'rmdir'],
|
||||||
|
command: 'rm -r dirname',
|
||||||
|
description: 'Remove directory recursively (replace "dirname") ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['copy file', 'cp file'],
|
||||||
|
command: 'cp source dest',
|
||||||
|
description: 'Copy file (replace "source" and "dest")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['move file', 'mv file', 'rename file'],
|
||||||
|
command: 'mv source dest',
|
||||||
|
description: 'Move/rename file (replace "source" and "dest")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Git
|
||||||
|
{
|
||||||
|
patterns: ['git status', 'check git', 'git changes'],
|
||||||
|
command: 'git status',
|
||||||
|
description: 'Show git working tree status',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git log', 'show commits', 'view history'],
|
||||||
|
command: 'git log --oneline -n 20',
|
||||||
|
description: 'Show recent commits',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git add', 'stage files', 'stage changes'],
|
||||||
|
command: 'git add .',
|
||||||
|
description: 'Stage all changes ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git commit', 'commit changes'],
|
||||||
|
command: 'git commit -m "message"',
|
||||||
|
description: 'Commit staged changes (replace "message")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git push', 'push changes'],
|
||||||
|
command: 'git push',
|
||||||
|
description: 'Push commits to remote ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git pull', 'pull changes'],
|
||||||
|
command: 'git pull',
|
||||||
|
description: 'Pull changes from remote ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git branch', 'list branches', 'show branches'],
|
||||||
|
command: 'git branch -a',
|
||||||
|
description: 'List all branches',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git checkout', 'switch branch'],
|
||||||
|
command: 'git checkout branchname',
|
||||||
|
description: 'Switch branch (replace "branchname")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git stash', 'stash changes'],
|
||||||
|
command: 'git stash',
|
||||||
|
description: 'Stash current changes',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['git diff', 'show diff', 'view changes'],
|
||||||
|
command: 'git diff',
|
||||||
|
description: 'Show unstaged changes',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Search
|
||||||
|
{
|
||||||
|
patterns: ['find file', 'search file', 'locate file'],
|
||||||
|
command: 'find . -name "pattern"',
|
||||||
|
description: 'Find files by name (replace "pattern")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['grep', 'search text', 'find text'],
|
||||||
|
command: 'grep -r "pattern" .',
|
||||||
|
description: 'Search text recursively (replace "pattern")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['search content', 'find in files'],
|
||||||
|
command: 'grep -rn "pattern" .',
|
||||||
|
description: 'Search with line numbers (replace "pattern")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Process management
|
||||||
|
{
|
||||||
|
patterns: ['list processes', 'show processes', 'ps'],
|
||||||
|
command: 'ps aux',
|
||||||
|
description: 'List all running processes',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['kill process', 'stop process'],
|
||||||
|
command: 'kill -9 PID',
|
||||||
|
description: 'Force kill process (replace "PID") ⚠️',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['port usage', 'check port', 'what port'],
|
||||||
|
command: 'lsof -i :PORT',
|
||||||
|
description: 'Check what is using port (replace "PORT")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Network
|
||||||
|
{
|
||||||
|
patterns: ['curl', 'http request', 'fetch url'],
|
||||||
|
command: 'curl -s https://example.com',
|
||||||
|
description: 'Make HTTP GET request (replace URL)',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['ping', 'check connection'],
|
||||||
|
command: 'ping -c 4 example.com',
|
||||||
|
description: 'Ping host (replace "example.com")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Disk
|
||||||
|
{
|
||||||
|
patterns: ['disk space', 'free space', 'df'],
|
||||||
|
command: 'df -h',
|
||||||
|
description: 'Show disk space usage',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['directory size', 'folder size', 'du'],
|
||||||
|
command: 'du -sh .',
|
||||||
|
description: 'Show current directory size',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// npm/yarn/pnpm
|
||||||
|
{
|
||||||
|
patterns: ['install packages', 'npm install', 'install deps'],
|
||||||
|
command: 'npm install',
|
||||||
|
description: 'Install npm dependencies',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['run dev', 'start dev', 'npm run dev'],
|
||||||
|
command: 'npm run dev',
|
||||||
|
description: 'Start development server',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['run build', 'build project', 'npm run build'],
|
||||||
|
command: 'npm run build',
|
||||||
|
description: 'Build project',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['run tests', 'npm test', 'test project'],
|
||||||
|
command: 'npm test',
|
||||||
|
description: 'Run tests',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Docker
|
||||||
|
{
|
||||||
|
patterns: ['docker ps', 'list containers', 'show containers'],
|
||||||
|
command: 'docker ps -a',
|
||||||
|
description: 'List all Docker containers',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['docker images', 'list images'],
|
||||||
|
command: 'docker images',
|
||||||
|
description: 'List Docker images',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['docker logs', 'container logs'],
|
||||||
|
command: 'docker logs container_name',
|
||||||
|
description: 'View container logs (replace "container_name")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Compression
|
||||||
|
{
|
||||||
|
patterns: ['tar gzip', 'compress folder', 'archive directory'],
|
||||||
|
command: 'tar -czvf archive.tar.gz dirname',
|
||||||
|
description: 'Compress directory to tar.gz (replace names)',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['extract tar', 'untar', 'decompress'],
|
||||||
|
command: 'tar -xzvf archive.tar.gz',
|
||||||
|
description: 'Extract tar.gz archive (replace "archive.tar.gz")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
// Permissions
|
||||||
|
{
|
||||||
|
patterns: ['chmod', 'change permissions', 'make executable'],
|
||||||
|
command: 'chmod +x filename',
|
||||||
|
description: 'Make file executable (replace "filename")',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['chown', 'change owner'],
|
||||||
|
command: 'chown user:group filename',
|
||||||
|
description: 'Change file owner (replace "user", "group", "filename")',
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
// Misc
|
||||||
|
{
|
||||||
|
patterns: ['current date', 'show date', 'what time'],
|
||||||
|
command: 'date',
|
||||||
|
description: 'Show current date and time',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['environment variables', 'show env', 'print env'],
|
||||||
|
command: 'env',
|
||||||
|
description: 'Show environment variables',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patterns: ['who am i', 'current user'],
|
||||||
|
command: 'whoami',
|
||||||
|
description: 'Show current user',
|
||||||
|
dangerous: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface TranslationResult {
|
||||||
|
readonly success: boolean
|
||||||
|
readonly command?: string
|
||||||
|
readonly description?: string
|
||||||
|
readonly dangerous?: boolean
|
||||||
|
readonly suggestions?: readonly string[]
|
||||||
|
readonly error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeInput(input: string): string {
|
||||||
|
return input.toLowerCase().trim().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateScore(input: string, patterns: readonly string[]): number {
|
||||||
|
const words = new Set(input.split(' '))
|
||||||
|
let bestScore = 0
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const patternWords = pattern.split(' ')
|
||||||
|
const matches = patternWords.filter(w => words.has(w)).length
|
||||||
|
const score = matches / Math.max(patternWords.length, words.size)
|
||||||
|
if (score > bestScore) bestScore = score
|
||||||
|
}
|
||||||
|
return bestScore
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translateCommand(input: string): TranslationResult {
|
||||||
|
const normalized = normalizeInput(input)
|
||||||
|
if (!normalized) {
|
||||||
|
return { success: false, error: 'Empty input' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let bestMatch: CommandPattern | null = null
|
||||||
|
let bestScore = 0
|
||||||
|
|
||||||
|
for (const pattern of COMMAND_PATTERNS) {
|
||||||
|
const score = calculateScore(normalized, pattern.patterns)
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score
|
||||||
|
bestMatch = pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch && bestScore >= 0.3) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
command: bestMatch.command,
|
||||||
|
description: bestMatch.description,
|
||||||
|
dangerous: bestMatch.dangerous,
|
||||||
|
suggestions: bestMatch.patterns.slice(0, 3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find related suggestions
|
||||||
|
const related = COMMAND_PATTERNS
|
||||||
|
.map(p => ({ pattern: p, score: calculateScore(normalized, p.patterns) }))
|
||||||
|
.filter(r => r.score > 0.1)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, 5)
|
||||||
|
.map(r => r.pattern.patterns[0])
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'No exact match found',
|
||||||
|
suggestions: related.length > 0 ? related : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllCommands(): readonly CommandPattern[] {
|
||||||
|
return COMMAND_PATTERNS
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
export interface ConflictBlock {
|
||||||
|
readonly ours: string
|
||||||
|
readonly theirs: string
|
||||||
|
readonly header: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolutionResult {
|
||||||
|
readonly success: boolean
|
||||||
|
readonly resolution?: string
|
||||||
|
readonly strategy?: 'ours' | 'theirs' | 'both' | 'manual'
|
||||||
|
readonly explanation: string
|
||||||
|
readonly conflictCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFLICT_START = '<<<<<<< '
|
||||||
|
const CONFLICT_DIVIDER = '======='
|
||||||
|
const CONFLICT_END = '>>>>>>> '
|
||||||
|
|
||||||
|
export function parseConflicts(input: string): ConflictBlock[] {
|
||||||
|
const lines = input.split('\n')
|
||||||
|
const conflicts: ConflictBlock[] = []
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].startsWith(CONFLICT_START)) {
|
||||||
|
const header = lines[i].slice(CONFLICT_START.length)
|
||||||
|
const oursLines: string[] = []
|
||||||
|
i++
|
||||||
|
while (i < lines.length && !lines[i].startsWith(CONFLICT_DIVIDER)) {
|
||||||
|
oursLines.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++ // skip divider
|
||||||
|
const theirsLines: string[] = []
|
||||||
|
while (i < lines.length && !lines[i].startsWith(CONFLICT_END)) {
|
||||||
|
theirsLines.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++ // skip end marker
|
||||||
|
conflicts.push({
|
||||||
|
ours: oursLines.join('\n'),
|
||||||
|
theirs: theirsLines.join('\n'),
|
||||||
|
header,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectStrategy(ours: string, theirs: string): 'ours' | 'theirs' | 'both' | 'manual' {
|
||||||
|
const ourLines = ours.trim().split('\n').filter(l => l.trim())
|
||||||
|
const theirLines = theirs.trim().split('\n').filter(l => l.trim())
|
||||||
|
|
||||||
|
// If one side is empty, take the other
|
||||||
|
if (ourLines.length === 0) return 'theirs'
|
||||||
|
if (theirLines.length === 0) return 'ours'
|
||||||
|
|
||||||
|
// If they're identical
|
||||||
|
if (ours.trim() === theirs.trim()) return 'ours'
|
||||||
|
|
||||||
|
// If one is a subset/superset of the other
|
||||||
|
if (ours.includes(theirs.trim()) && theirs.trim().length > 0) return 'ours'
|
||||||
|
if (theirs.includes(ours.trim()) && ours.trim().length > 0) return 'theirs'
|
||||||
|
|
||||||
|
// Check for import/dependency additions (common pattern)
|
||||||
|
const ourImports = ourLines.filter(l => l.trim().startsWith('import '))
|
||||||
|
const theirImports = theirLines.filter(l => l.trim().startsWith('import '))
|
||||||
|
if (ourImports.length > 0 || theirImports.length > 0) {
|
||||||
|
return 'both'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'manual'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBlock(block: ConflictBlock, strategy: 'ours' | 'theirs' | 'both' | 'manual'): string {
|
||||||
|
switch (strategy) {
|
||||||
|
case 'ours':
|
||||||
|
return block.ours
|
||||||
|
case 'theirs':
|
||||||
|
return block.theirs
|
||||||
|
case 'both':
|
||||||
|
// Merge non-duplicate lines
|
||||||
|
const ourLines = block.ours.split('\n')
|
||||||
|
const theirLines = block.theirs.split('\n')
|
||||||
|
const merged = new Set<string>()
|
||||||
|
const result: string[] = []
|
||||||
|
for (const line of [...ourLines, ...theirLines]) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || merged.has(trimmed)) continue
|
||||||
|
merged.add(trimmed)
|
||||||
|
result.push(line)
|
||||||
|
}
|
||||||
|
return result.join('\n')
|
||||||
|
case 'manual':
|
||||||
|
return `/* CONFLICT: ${block.header} */\n/* YOURS */\n${block.ours}\n/* THEIRS */\n${block.theirs}\n/* END CONFLICT */`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveConflicts(
|
||||||
|
input: string,
|
||||||
|
strategy?: 'ours' | 'theirs' | 'both' | 'manual'
|
||||||
|
): ResolutionResult {
|
||||||
|
const conflicts = parseConflicts(input)
|
||||||
|
|
||||||
|
if (conflicts.length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
explanation: 'No git conflict markers found in the input.',
|
||||||
|
conflictCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = input.split('\n')
|
||||||
|
const result: string[] = []
|
||||||
|
let i = 0
|
||||||
|
let conflictIndex = 0
|
||||||
|
let autoResolved = 0
|
||||||
|
let manualNeeded = 0
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
if (lines[i].startsWith(CONFLICT_START)) {
|
||||||
|
const block = conflicts[conflictIndex]
|
||||||
|
const detected = strategy || detectStrategy(block.ours, block.theirs)
|
||||||
|
const resolved = resolveBlock(block, detected)
|
||||||
|
result.push(resolved)
|
||||||
|
|
||||||
|
if (detected === 'manual') manualNeeded++
|
||||||
|
else autoResolved++
|
||||||
|
|
||||||
|
// Skip past conflict markers
|
||||||
|
while (i < lines.length && !lines[i].startsWith(CONFLICT_END)) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++ // skip end marker
|
||||||
|
conflictIndex++
|
||||||
|
} else {
|
||||||
|
result.push(lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedStrategy = strategy || detectStrategy(conflicts[0].ours, conflicts[0].theirs)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
resolution: result.join('\n'),
|
||||||
|
strategy: usedStrategy,
|
||||||
|
conflictCount: conflicts.length,
|
||||||
|
explanation:
|
||||||
|
autoResolved > 0 && manualNeeded > 0
|
||||||
|
? `Auto-resolved ${autoResolved} of ${conflicts.length} conflicts. ${manualNeeded} need manual review.`
|
||||||
|
: manualNeeded > 0
|
||||||
|
? `All ${conflicts.length} conflicts need manual review.`
|
||||||
|
: `All ${conflicts.length} conflicts auto-resolved using '${usedStrategy}' strategy.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
export interface RegexTestResult {
|
||||||
|
readonly valid: boolean
|
||||||
|
readonly matches: readonly RegexMatch[]
|
||||||
|
readonly error?: string
|
||||||
|
readonly replacePreview?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexMatch {
|
||||||
|
readonly full: string
|
||||||
|
readonly groups: readonly (string | undefined)[]
|
||||||
|
readonly index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegexTestInput {
|
||||||
|
readonly pattern: string
|
||||||
|
readonly flags: string
|
||||||
|
readonly text: string
|
||||||
|
readonly replacement?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testRegex(input: RegexTestInput): RegexTestResult {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(input.pattern, input.flags)
|
||||||
|
const matches: RegexMatch[] = []
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
|
||||||
|
if (input.flags.includes('g')) {
|
||||||
|
while ((match = regex.exec(input.text)) !== null) {
|
||||||
|
if (match.index === regex.lastIndex) regex.lastIndex++
|
||||||
|
matches.push({
|
||||||
|
full: match[0],
|
||||||
|
groups: match.slice(1),
|
||||||
|
index: match.index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match = regex.exec(input.text)
|
||||||
|
if (match) {
|
||||||
|
matches.push({
|
||||||
|
full: match[0],
|
||||||
|
groups: match.slice(1),
|
||||||
|
index: match.index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let replacePreview: string | undefined
|
||||||
|
if (input.replacement !== undefined) {
|
||||||
|
replacePreview = input.text.replace(regex, input.replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, matches, replacePreview }
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
matches: [],
|
||||||
|
error: err instanceof Error ? err.message : 'Invalid regex',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function explainRegex(pattern: string): string {
|
||||||
|
const explanations: Record<string, string> = {
|
||||||
|
'^': 'Start of string',
|
||||||
|
'$': 'End of string',
|
||||||
|
'.': 'Any character except newline',
|
||||||
|
'\\d': 'Digit (0-9)',
|
||||||
|
'\\D': 'Non-digit',
|
||||||
|
'\\w': 'Word character (a-z, A-Z, 0-9, _)',
|
||||||
|
'\\W': 'Non-word character',
|
||||||
|
'\\s': 'Whitespace',
|
||||||
|
'\\S': 'Non-whitespace',
|
||||||
|
'\\n': 'Newline',
|
||||||
|
'\\t': 'Tab',
|
||||||
|
'\\b': 'Word boundary',
|
||||||
|
'+': 'One or more of preceding',
|
||||||
|
'*': 'Zero or more of preceding',
|
||||||
|
'?': 'Zero or one of preceding',
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
for (const [token, meaning] of Object.entries(explanations)) {
|
||||||
|
if (pattern.includes(token)) {
|
||||||
|
parts.push(`${token} = ${meaning}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) return 'Pattern found'
|
||||||
|
return parts.join('; ')
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export interface ExtensionSettings {
|
||||||
|
showDangerWarning: boolean
|
||||||
|
copyOnTranslate: boolean
|
||||||
|
preferredShell: 'bash' | 'zsh' | 'fish' | 'powershell'
|
||||||
|
theme: 'light' | 'dark' | 'auto'
|
||||||
|
contextMenuEnabled: boolean
|
||||||
|
inlinePanelEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: ExtensionSettings = {
|
||||||
|
showDangerWarning: true,
|
||||||
|
copyOnTranslate: true,
|
||||||
|
preferredShell: 'bash',
|
||||||
|
theme: 'auto',
|
||||||
|
contextMenuEnabled: true,
|
||||||
|
inlinePanelEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSettings(): Promise<ExtensionSettings> {
|
||||||
|
try {
|
||||||
|
const result = await chrome.storage.sync.get('screwdriver_settings')
|
||||||
|
return { ...DEFAULT_SETTINGS, ...(result.screwdriver_settings ?? {}) }
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_SETTINGS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSettings(settings: Partial<ExtensionSettings>): Promise<void> {
|
||||||
|
const current = await getSettings()
|
||||||
|
await chrome.storage.sync.set({
|
||||||
|
screwdriver_settings: { ...current, ...settings },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipboardItem {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
timestamp: number
|
||||||
|
type: 'command' | 'transform' | 'conflict' | 'regex'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addClipboardItem(item: Omit<ClipboardItem, 'id' | 'timestamp'>): Promise<void> {
|
||||||
|
const result = await chrome.storage.local.get('screwdriver_history')
|
||||||
|
const history: ClipboardItem[] = result.screwdriver_history ?? []
|
||||||
|
const newItem: ClipboardItem = {
|
||||||
|
...item,
|
||||||
|
id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
const updated = [newItem, ...history].slice(0, 50)
|
||||||
|
await chrome.storage.local.set({ screwdriver_history: updated })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getClipboardHistory(): Promise<ClipboardItem[]> {
|
||||||
|
const result = await chrome.storage.local.get('screwdriver_history')
|
||||||
|
return result.screwdriver_history ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearClipboardHistory(): Promise<void> {
|
||||||
|
await chrome.storage.local.remove('screwdriver_history')
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
export type TransformType =
|
||||||
|
| 'uppercase'
|
||||||
|
| 'lowercase'
|
||||||
|
| 'titlecase'
|
||||||
|
| 'camelcase'
|
||||||
|
| 'pascalcase'
|
||||||
|
| 'snakecase'
|
||||||
|
| 'kebabcase'
|
||||||
|
| 'constantcase'
|
||||||
|
| 'base64encode'
|
||||||
|
| 'base64decode'
|
||||||
|
| 'urlescape'
|
||||||
|
| 'urlunescape'
|
||||||
|
| 'trim'
|
||||||
|
| 'trimlines'
|
||||||
|
| 'dedupe'
|
||||||
|
| 'sortlines'
|
||||||
|
| 'reverselines'
|
||||||
|
| 'jsonpretty'
|
||||||
|
| 'jsoncompact'
|
||||||
|
| 'jsontocsv'
|
||||||
|
| 'csvtojson'
|
||||||
|
|
||||||
|
export interface TransformResult {
|
||||||
|
readonly success: boolean
|
||||||
|
readonly result: string
|
||||||
|
readonly error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformOption {
|
||||||
|
readonly type: TransformType
|
||||||
|
readonly label: string
|
||||||
|
readonly description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TRANSFORM_OPTIONS: readonly TransformOption[] = [
|
||||||
|
{ type: 'uppercase', label: 'UPPERCASE', description: 'Convert to uppercase' },
|
||||||
|
{ type: 'lowercase', label: 'lowercase', description: 'Convert to lowercase' },
|
||||||
|
{ type: 'titlecase', label: 'Title Case', description: 'Capitalize Each Word' },
|
||||||
|
{ type: 'camelcase', label: 'camelCase', description: 'camelCaseNotation' },
|
||||||
|
{ type: 'pascalcase', label: 'PascalCase', description: 'PascalCaseNotation' },
|
||||||
|
{ type: 'snakecase', label: 'snake_case', description: 'snake_case_notation' },
|
||||||
|
{ type: 'kebabcase', label: 'kebab-case', description: 'kebab-case-notation' },
|
||||||
|
{ type: 'constantcase', label: 'CONSTANT_CASE', description: 'CONSTANT_CASE_NOTATION' },
|
||||||
|
{ type: 'base64encode', label: 'Base64 Encode', description: 'Encode to Base64' },
|
||||||
|
{ type: 'base64decode', label: 'Base64 Decode', description: 'Decode from Base64' },
|
||||||
|
{ type: 'urlescape', label: 'URL Encode', description: 'Percent-encode string' },
|
||||||
|
{ type: 'urlunescape', label: 'URL Decode', description: 'Percent-decode string' },
|
||||||
|
{ type: 'trim', label: 'Trim', description: 'Remove leading/trailing whitespace' },
|
||||||
|
{ type: 'trimlines', label: 'Trim Lines', description: 'Remove leading/trailing whitespace from each line' },
|
||||||
|
{ type: 'dedupe', label: 'Remove Duplicates', description: 'Remove duplicate lines' },
|
||||||
|
{ type: 'sortlines', label: 'Sort Lines', description: 'Sort lines alphabetically' },
|
||||||
|
{ type: 'reverselines', label: 'Reverse Lines', description: 'Reverse line order' },
|
||||||
|
{ type: 'jsonpretty', label: 'JSON Pretty', description: 'Format JSON with indentation' },
|
||||||
|
{ type: 'jsoncompact', label: 'JSON Compact', description: 'Minify JSON' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function toTitleCase(str: string): string {
|
||||||
|
return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCamelCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/[^a-zA-Z0-9]+(.)/g, (_, chr) => chr.toUpperCase())
|
||||||
|
.replace(/^[A-Z]/, chr => chr.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPascalCase(str: string): string {
|
||||||
|
const camel = toCamelCase(str)
|
||||||
|
return camel.charAt(0).toUpperCase() + camel.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSnakeCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toKebabCase(str: string): string {
|
||||||
|
return toSnakeCase(str).replace(/_/g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toConstantCase(str: string): string {
|
||||||
|
return toSnakeCase(str).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformText(text: string, type: TransformType): TransformResult {
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'uppercase':
|
||||||
|
return { success: true, result: text.toUpperCase() }
|
||||||
|
case 'lowercase':
|
||||||
|
return { success: true, result: text.toLowerCase() }
|
||||||
|
case 'titlecase':
|
||||||
|
return { success: true, result: toTitleCase(text) }
|
||||||
|
case 'camelcase':
|
||||||
|
return { success: true, result: toCamelCase(text) }
|
||||||
|
case 'pascalcase':
|
||||||
|
return { success: true, result: toPascalCase(text) }
|
||||||
|
case 'snakecase':
|
||||||
|
return { success: true, result: toSnakeCase(text) }
|
||||||
|
case 'kebabcase':
|
||||||
|
return { success: true, result: toKebabCase(text) }
|
||||||
|
case 'constantcase':
|
||||||
|
return { success: true, result: toConstantCase(text) }
|
||||||
|
case 'base64encode':
|
||||||
|
return { success: true, result: btoa(unescape(encodeURIComponent(text))) }
|
||||||
|
case 'base64decode': {
|
||||||
|
const decoded = decodeURIComponent(escape(atob(text)))
|
||||||
|
return { success: true, result: decoded }
|
||||||
|
}
|
||||||
|
case 'urlescape':
|
||||||
|
return { success: true, result: encodeURIComponent(text) }
|
||||||
|
case 'urlunescape':
|
||||||
|
return { success: true, result: decodeURIComponent(text) }
|
||||||
|
case 'trim':
|
||||||
|
return { success: true, result: text.trim() }
|
||||||
|
case 'trimlines':
|
||||||
|
return { success: true, result: text.split('\n').map(l => l.trim()).join('\n') }
|
||||||
|
case 'dedupe': {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return { success: true, result: text.split('\n').filter(l => { const k = l.trim(); if (!k || seen.has(k)) return false; seen.add(k); return true }).join('\n') }
|
||||||
|
}
|
||||||
|
case 'sortlines':
|
||||||
|
return { success: true, result: text.split('\n').sort().join('\n') }
|
||||||
|
case 'reverselines':
|
||||||
|
return { success: true, result: text.split('\n').reverse().join('\n') }
|
||||||
|
case 'jsonpretty': {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return { success: true, result: JSON.stringify(parsed, null, 2) }
|
||||||
|
}
|
||||||
|
case 'jsoncompact': {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return { success: true, result: JSON.stringify(parsed) }
|
||||||
|
}
|
||||||
|
case 'jsontocsv': {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||||
|
return { success: false, result: '', error: 'JSON must be a non-empty array of objects' }
|
||||||
|
}
|
||||||
|
const keys = Object.keys(parsed[0])
|
||||||
|
const header = keys.join(',')
|
||||||
|
const rows = parsed.map((row: Record<string, unknown>) =>
|
||||||
|
keys.map(k => {
|
||||||
|
const val = String(row[k] ?? '')
|
||||||
|
if (val.includes(',') || val.includes('"') || val.includes('\n')) {
|
||||||
|
return `"${val.replace(/"/g, '""')}"`
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}).join(',')
|
||||||
|
)
|
||||||
|
return { success: true, result: [header, ...rows].join('\n') }
|
||||||
|
}
|
||||||
|
case 'csvtojson': {
|
||||||
|
const lines = text.split('\n').filter(l => l.trim())
|
||||||
|
if (lines.length < 2) {
|
||||||
|
return { success: false, result: '', error: 'CSV must have header + at least one data row' }
|
||||||
|
}
|
||||||
|
const parseCSVLine = (line: string): string[] => {
|
||||||
|
const result: string[] = []
|
||||||
|
let current = ''
|
||||||
|
let inQuotes = false
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i]
|
||||||
|
if (char === '"') {
|
||||||
|
if (inQuotes && line[i + 1] === '"') {
|
||||||
|
current += '"'
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
}
|
||||||
|
} else if (char === ',' && !inQuotes) {
|
||||||
|
result.push(current.trim())
|
||||||
|
current = ''
|
||||||
|
} else {
|
||||||
|
current += char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(current.trim())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
const headers = parseCSVLine(lines[0])
|
||||||
|
const data = lines.slice(1).map(line => {
|
||||||
|
const values = parseCSVLine(line)
|
||||||
|
const obj: Record<string, string> = {}
|
||||||
|
headers.forEach((h, i) => { obj[h] = values[i] ?? '' })
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
return { success: true, result: JSON.stringify(data, null, 2) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, result: '', error: err instanceof Error ? err.message : 'Unknown error' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { translateCommand, getAllCommands } from '../src/utils/command-translator.js'
|
||||||
|
|
||||||
|
describe('command-translator', () => {
|
||||||
|
it('translates simple commands', () => {
|
||||||
|
const r = translateCommand('list files')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.command).toBe('ls -la')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('translates git commands', () => {
|
||||||
|
const r = translateCommand('git status')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.command).toBe('git status')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks dangerous commands', () => {
|
||||||
|
const r = translateCommand('remove directory')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.dangerous).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles unknown input gracefully', () => {
|
||||||
|
const r = translateCommand('xyz')
|
||||||
|
expect(r.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles empty input', () => {
|
||||||
|
const r = translateCommand('')
|
||||||
|
expect(r.success).toBe(false)
|
||||||
|
expect(r.error).toBe('Empty input')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a command catalog', () => {
|
||||||
|
const cmds = getAllCommands()
|
||||||
|
expect(cmds.length).toBeGreaterThan(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { parseConflicts, resolveConflicts } from '../src/utils/git-conflict-resolver.js'
|
||||||
|
|
||||||
|
const CONFLICT_TEXT = `import { foo } from './foo'
|
||||||
|
<<<<<<< HEAD
|
||||||
|
const x = 1
|
||||||
|
=======
|
||||||
|
const x = 2
|
||||||
|
>>>>>>> feature
|
||||||
|
export { x }`
|
||||||
|
|
||||||
|
describe('git-conflict-resolver', () => {
|
||||||
|
it('parses conflicts', () => {
|
||||||
|
const conflicts = parseConflicts(CONFLICT_TEXT)
|
||||||
|
expect(conflicts.length).toBe(1)
|
||||||
|
expect(conflicts[0].ours).toBe('const x = 1')
|
||||||
|
expect(conflicts[0].theirs).toBe('const x = 2')
|
||||||
|
expect(conflicts[0].header).toBe('HEAD')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-resolves with ours strategy', () => {
|
||||||
|
const result = resolveConflicts(CONFLICT_TEXT, 'ours')
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.resolution).toContain('const x = 1')
|
||||||
|
expect(result.resolution).not.toContain('<<<<<<<')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('auto-resolves with theirs strategy', () => {
|
||||||
|
const result = resolveConflicts(CONFLICT_TEXT, 'theirs')
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.resolution).toContain('const x = 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects no conflicts', () => {
|
||||||
|
const result = resolveConflicts('no conflict here')
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
expect(result.explanation).toContain('No git conflict markers')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles multiple conflicts', () => {
|
||||||
|
const multi = `${CONFLICT_TEXT}\n\n${CONFLICT_TEXT.replace('x', 'y')}`
|
||||||
|
const result = resolveConflicts(multi, 'ours')
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(result.conflictCount).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { testRegex, explainRegex } from '../src/utils/regex-tester.js'
|
||||||
|
|
||||||
|
describe('regex-tester', () => {
|
||||||
|
it('finds simple matches', () => {
|
||||||
|
const result = testRegex({ pattern: '\\d+', flags: 'g', text: 'abc 123 def 456' })
|
||||||
|
expect(result.valid).toBe(true)
|
||||||
|
expect(result.matches.length).toBe(2)
|
||||||
|
expect(result.matches[0].full).toBe('123')
|
||||||
|
expect(result.matches[1].full).toBe('456')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('captures groups', () => {
|
||||||
|
const result = testRegex({ pattern: '(\\w+)=(\\d+)', flags: 'g', text: 'a=1,b=2' })
|
||||||
|
expect(result.valid).toBe(true)
|
||||||
|
expect(result.matches[0].groups).toEqual(['a', '1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports invalid regex', () => {
|
||||||
|
const result = testRegex({ pattern: '[', flags: '', text: 'test' })
|
||||||
|
expect(result.valid).toBe(false)
|
||||||
|
expect(result.error).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows replace preview', () => {
|
||||||
|
const result = testRegex({
|
||||||
|
pattern: '\\d+',
|
||||||
|
flags: 'g',
|
||||||
|
text: 'abc 123',
|
||||||
|
replacement: 'XXX',
|
||||||
|
})
|
||||||
|
expect(result.replacePreview).toBe('abc XXX')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('explains patterns', () => {
|
||||||
|
const exp = explainRegex('\\d+')
|
||||||
|
expect(exp).toContain('\\d')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { transformText, TRANSFORM_OPTIONS } from '../src/utils/text-transformer.js'
|
||||||
|
|
||||||
|
describe('text-transformer', () => {
|
||||||
|
it('converts to uppercase', () => {
|
||||||
|
const r = transformText('hello world', 'uppercase')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('HELLO WORLD')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('converts to camelCase', () => {
|
||||||
|
const r = transformText('hello world', 'camelcase')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('helloWorld')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('base64 encodes', () => {
|
||||||
|
const r = transformText('hello', 'base64encode')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('aGVsbG8=')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('base64 decodes', () => {
|
||||||
|
const r = transformText('aGVsbG8=', 'base64decode')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('formats JSON', () => {
|
||||||
|
const r = transformText('{"a":1}', 'jsonpretty')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toContain('"a": 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('minifies JSON', () => {
|
||||||
|
const r = transformText('{\n "a": 1\n}', 'jsoncompact')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('{"a":1}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sorts lines', () => {
|
||||||
|
const r = transformText('c\nb\na', 'sortlines')
|
||||||
|
expect(r.success).toBe(true)
|
||||||
|
expect(r.result).toBe('a\nb\nc')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has transform options', () => {
|
||||||
|
expect(TRANSFORM_OPTIONS.length).toBeGreaterThan(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles invalid JSON', () => {
|
||||||
|
const r = transformText('not json', 'jsonpretty')
|
||||||
|
expect(r.success).toBe(false)
|
||||||
|
expect(r.error).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"types": ["chrome", "node", "vitest/globals"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { crx } from '@crxjs/vite-plugin'
|
||||||
|
import manifest from './manifest.json' with { type: 'json' }
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
popup: 'src/popup/popup.html',
|
||||||
|
options: 'src/options/options.html',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [crx({ manifest })],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user