From 52fed8c1d3e0b0c5fcb9e8cc04de8337043373d0 Mon Sep 17 00:00:00 2001 From: CD Date: Sat, 7 Mar 2026 11:32:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(phase3):=20VS=20Code=20Extension=20?= =?UTF-8?q?=EC=8A=A4=EC=BA=90=ED=8F=B4=EB=93=9C=20-=20bridge=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(approve/reject/text=20relay)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + extension/out/extension.js | 292 +++++++++++++++++++++++++++++++++ extension/out/extension.js.map | 1 + extension/package-lock.json | 58 +++++++ extension/package.json | 56 +++++++ extension/src/extension.ts | 285 ++++++++++++++++++++++++++++++++ extension/tsconfig.json | 18 ++ 7 files changed, 714 insertions(+) create mode 100644 extension/out/extension.js create mode 100644 extension/out/extension.js.map create mode 100644 extension/package-lock.json create mode 100644 extension/package.json create mode 100644 extension/src/extension.ts create mode 100644 extension/tsconfig.json diff --git a/.gitignore b/.gitignore index 58b577e..d4a1120 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ Thumbs.db # Logs *.log + +# Node +node_modules/ +extension/out/ diff --git a/extension/out/extension.js b/extension/out/extension.js new file mode 100644 index 0000000..31dd5cb --- /dev/null +++ b/extension/out/extension.js @@ -0,0 +1,292 @@ +"use strict"; +/** + * Gravity Bridge — VS Code Extension + * + * Bridges Antigravity IDE approval dialogs to the Discord bot via file-based protocol. + * + * Flow: + * 1. Extension watches for tool approval notifications in VS Code + * 2. Writes pending approval to bridge/pending/ + * 3. Discord bot sends buttons to user + * 4. User clicks approve/reject + * 5. Bot writes response to bridge/response/ + * 6. Extension reads response → sends keyboard command to approve/reject + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.activate = activate; +exports.writePendingApproval = writePendingApproval; +exports.deactivate = deactivate; +const vscode = __importStar(require("vscode")); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const os = __importStar(require("os")); +let watcher = null; +let statusBar; +let bridgePath; +let isActive = false; +// Track pending approvals we've already sent +const sentPendingIds = new Set(); +function activate(context) { + console.log('Gravity Bridge: activating...'); + // Determine bridge path + const config = vscode.workspace.getConfiguration('gravityBridge'); + const configPath = config.get('bridgePath'); + bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge'); + // Ensure bridge directories exist + const dirs = ['pending', 'response', 'commands']; + for (const dir of dirs) { + const dirPath = path.join(bridgePath, dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + // Status bar + statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + statusBar.command = 'gravityBridge.start'; + statusBar.text = '$(radio-tower) Bridge: Off'; + statusBar.tooltip = 'Gravity Bridge — Click to start'; + statusBar.show(); + context.subscriptions.push(statusBar); + // Register commands + context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.start', startBridge), vscode.commands.registerCommand('gravityBridge.stop', stopBridge), vscode.commands.registerCommand('gravityBridge.approve', () => handleManualAction(true)), vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false))); + // Auto-start + startBridge(); +} +function startBridge() { + if (isActive) { + vscode.window.showInformationMessage('Gravity Bridge is already running'); + return; + } + isActive = true; + statusBar.text = '$(radio-tower) Bridge: On'; + statusBar.tooltip = 'Gravity Bridge — Active'; + statusBar.command = 'gravityBridge.stop'; + // Watch bridge/response/ for Discord user responses + const responsePath = path.join(bridgePath, 'response'); + try { + watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => { + if (eventType === 'rename' && filename && filename.endsWith('.json')) { + handleResponse(path.join(responsePath, filename)); + } + }); + console.log('Gravity Bridge: watching response directory'); + } + catch (err) { + console.error('Gravity Bridge: failed to watch response dir', err); + } + // Watch for commands (user text input from Discord) + const commandsPath = path.join(bridgePath, 'commands'); + try { + fs.watch(commandsPath, { persistent: false }, (eventType, filename) => { + if (eventType === 'rename' && filename && filename.endsWith('.json')) { + handleCommand(path.join(commandsPath, filename)); + } + }); + console.log('Gravity Bridge: watching commands directory'); + } + catch (err) { + console.error('Gravity Bridge: failed to watch commands dir', err); + } + vscode.window.showInformationMessage('Gravity Bridge: Started'); + console.log(`Gravity Bridge: started, bridge path: ${bridgePath}`); +} +function stopBridge() { + if (!isActive) { + return; + } + isActive = false; + statusBar.text = '$(radio-tower) Bridge: Off'; + statusBar.tooltip = 'Gravity Bridge — Click to start'; + statusBar.command = 'gravityBridge.start'; + if (watcher) { + watcher.close(); + watcher = null; + } + vscode.window.showInformationMessage('Gravity Bridge: Stopped'); +} +/** + * Handle a response from Discord (approve/reject). + * Reads the response JSON and simulates the appropriate action. + */ +async function handleResponse(filePath) { + try { + // Small delay to ensure file is fully written + await new Promise(resolve => setTimeout(resolve, 200)); + if (!fs.existsSync(filePath)) { + return; + } + const content = fs.readFileSync(filePath, 'utf-8'); + const response = JSON.parse(content); + if (response.approved === undefined) { + return; + } + console.log(`Gravity Bridge: response received — approved=${response.approved}`); + if (response.approved) { + // Simulate pressing Enter or clicking approve + // Strategy: Use VS Code command to accept suggestion + await simulateApproval(); + vscode.window.showInformationMessage(`✅ Approved: ${response.request_id}`); + } + else { + await simulateRejection(); + vscode.window.showInformationMessage(`❌ Rejected: ${response.request_id}`); + } + // Cleanup: delete the response file after processing + try { + fs.unlinkSync(filePath); + } + catch (e) { /* ignore */ } + } + catch (err) { + console.error('Gravity Bridge: error handling response', err); + } +} +/** + * Handle a text command from Discord. + * Types the text into the active editor or chat input. + */ +async function handleCommand(filePath) { + try { + await new Promise(resolve => setTimeout(resolve, 200)); + if (!fs.existsSync(filePath)) { + return; + } + const content = fs.readFileSync(filePath, 'utf-8'); + const command = JSON.parse(content); + if (command.consumed || !command.text) { + return; + } + console.log(`Gravity Bridge: command received — "${command.text.substring(0, 50)}..."`); + // Type into the active text input (chat panel) + await vscode.commands.executeCommand('workbench.action.chat.open'); + // Small delay for chat panel to open + await new Promise(resolve => setTimeout(resolve, 500)); + // Type the text using clipboard + const oldClipboard = await vscode.env.clipboard.readText(); + await vscode.env.clipboard.writeText(command.text); + await vscode.commands.executeCommand('editor.action.clipboardPasteAction'); + await vscode.env.clipboard.writeText(oldClipboard); + // Mark as consumed + command.consumed = true; + fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8'); + vscode.window.showInformationMessage(`📨 Discord input: ${command.text.substring(0, 50)}...`); + } + catch (err) { + console.error('Gravity Bridge: error handling command', err); + } +} +/** + * Simulate approval — try multiple strategies. + */ +async function simulateApproval() { + try { + // Strategy 1: Try executing the accept command if available + await vscode.commands.executeCommand('workbench.action.acceptSelectedCodeAction'); + } + catch { + // Strategy 2: Send Enter key via type command + try { + await vscode.commands.executeCommand('type', { text: '\n' }); + } + catch { + // Strategy 3: Focus terminal and send Enter + await vscode.commands.executeCommand('workbench.action.terminal.focus'); + } + } +} +/** + * Simulate rejection — try multiple strategies. + */ +async function simulateRejection() { + try { + // Strategy 1: Escape key + await vscode.commands.executeCommand('workbench.action.closeQuickOpen'); + } + catch { + try { + await vscode.commands.executeCommand('cancelSelection'); + } + catch { + // Fallback: just notify + console.log('Gravity Bridge: rejection sent but no active dialog found'); + } + } +} +/** + * Manual approve/reject from command palette. + * Writes a pending request for testing purposes. + */ +function handleManualAction(approved) { + const requestId = `manual-${Date.now()}`; + const responsePath = path.join(bridgePath, 'response', `${requestId}.json`); + const response = { + request_id: requestId, + approved: approved, + user_input: '', + timestamp: Date.now() / 1000, + }; + fs.writeFileSync(responsePath, JSON.stringify(response, null, 2), 'utf-8'); + if (approved) { + simulateApproval(); + } + else { + simulateRejection(); + } +} +/** + * Write a pending approval request to bridge/pending/ for Discord bot to pick up. + */ +function writePendingApproval(conversationId, command, description) { + const requestId = `req-${Date.now()}`; + const pendingPath = path.join(bridgePath, 'pending', `${requestId}.json`); + const request = { + request_id: requestId, + conversation_id: conversationId, + command: command, + description: description, + timestamp: Date.now() / 1000, + status: 'pending', + discord_message_id: 0, + }; + fs.writeFileSync(pendingPath, JSON.stringify(request, null, 2), 'utf-8'); + sentPendingIds.add(requestId); + console.log(`Gravity Bridge: pending approval written — ${requestId}`); + return requestId; +} +function deactivate() { + stopBridge(); +} +//# sourceMappingURL=extension.js.map \ No newline at end of file diff --git a/extension/out/extension.js.map b/extension/out/extension.js.map new file mode 100644 index 0000000..c1d5ef0 --- /dev/null +++ b/extension/out/extension.js.map @@ -0,0 +1 @@ +{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extension/package-lock.json b/extension/package-lock.json new file mode 100644 index 0000000..ed205c3 --- /dev/null +++ b/extension/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "gravity-bridge", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gravity-bridge", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.80.0", + "typescript": "^5.3.0" + }, + "engines": { + "vscode": "^1.80.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.109.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", + "integrity": "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 0000000..ba73fe6 --- /dev/null +++ b/extension/package.json @@ -0,0 +1,56 @@ +{ + "name": "gravity-bridge", + "displayName": "Gravity Bridge", + "description": "Antigravity ↔ Discord 브리지 연동 확장", + "version": "0.1.0", + "publisher": "variet", + "engines": { + "vscode": "^1.80.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/vscode": "^1.80.0", + "@types/node": "^20.0.0", + "typescript": "^5.3.0" + }, + "contributes": { + "commands": [ + { + "command": "gravityBridge.start", + "title": "Gravity Bridge: Start" + }, + { + "command": "gravityBridge.stop", + "title": "Gravity Bridge: Stop" + }, + { + "command": "gravityBridge.approve", + "title": "Gravity Bridge: Approve Pending" + }, + { + "command": "gravityBridge.reject", + "title": "Gravity Bridge: Reject Pending" + } + ], + "configuration": { + "title": "Gravity Bridge", + "properties": { + "gravityBridge.bridgePath": { + "type": "string", + "default": "", + "description": "Bridge 디렉토리 경로 (기본: ~/.gemini/antigravity/bridge)" + } + } + } + } +} \ No newline at end of file diff --git a/extension/src/extension.ts b/extension/src/extension.ts new file mode 100644 index 0000000..bafcf85 --- /dev/null +++ b/extension/src/extension.ts @@ -0,0 +1,285 @@ +/** + * Gravity Bridge — VS Code Extension + * + * Bridges Antigravity IDE approval dialogs to the Discord bot via file-based protocol. + * + * Flow: + * 1. Extension watches for tool approval notifications in VS Code + * 2. Writes pending approval to bridge/pending/ + * 3. Discord bot sends buttons to user + * 4. User clicks approve/reject + * 5. Bot writes response to bridge/response/ + * 6. Extension reads response → sends keyboard command to approve/reject + */ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +let watcher: fs.FSWatcher | null = null; +let statusBar: vscode.StatusBarItem; +let bridgePath: string; +let isActive = false; + +// Track pending approvals we've already sent +const sentPendingIds = new Set(); + +export function activate(context: vscode.ExtensionContext) { + console.log('Gravity Bridge: activating...'); + + // Determine bridge path + const config = vscode.workspace.getConfiguration('gravityBridge'); + const configPath = config.get('bridgePath'); + bridgePath = configPath || path.join(os.homedir(), '.gemini', 'antigravity', 'bridge'); + + // Ensure bridge directories exist + const dirs = ['pending', 'response', 'commands']; + for (const dir of dirs) { + const dirPath = path.join(bridgePath, dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } + + // Status bar + statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + statusBar.command = 'gravityBridge.start'; + statusBar.text = '$(radio-tower) Bridge: Off'; + statusBar.tooltip = 'Gravity Bridge — Click to start'; + statusBar.show(); + context.subscriptions.push(statusBar); + + // Register commands + context.subscriptions.push( + vscode.commands.registerCommand('gravityBridge.start', startBridge), + vscode.commands.registerCommand('gravityBridge.stop', stopBridge), + vscode.commands.registerCommand('gravityBridge.approve', () => handleManualAction(true)), + vscode.commands.registerCommand('gravityBridge.reject', () => handleManualAction(false)), + ); + + // Auto-start + startBridge(); +} + +function startBridge() { + if (isActive) { + vscode.window.showInformationMessage('Gravity Bridge is already running'); + return; + } + + isActive = true; + statusBar.text = '$(radio-tower) Bridge: On'; + statusBar.tooltip = 'Gravity Bridge — Active'; + statusBar.command = 'gravityBridge.stop'; + + // Watch bridge/response/ for Discord user responses + const responsePath = path.join(bridgePath, 'response'); + try { + watcher = fs.watch(responsePath, { persistent: false }, (eventType, filename) => { + if (eventType === 'rename' && filename && filename.endsWith('.json')) { + handleResponse(path.join(responsePath, filename)); + } + }); + console.log('Gravity Bridge: watching response directory'); + } catch (err) { + console.error('Gravity Bridge: failed to watch response dir', err); + } + + // Watch for commands (user text input from Discord) + const commandsPath = path.join(bridgePath, 'commands'); + try { + fs.watch(commandsPath, { persistent: false }, (eventType, filename) => { + if (eventType === 'rename' && filename && filename.endsWith('.json')) { + handleCommand(path.join(commandsPath, filename)); + } + }); + console.log('Gravity Bridge: watching commands directory'); + } catch (err) { + console.error('Gravity Bridge: failed to watch commands dir', err); + } + + vscode.window.showInformationMessage('Gravity Bridge: Started'); + console.log(`Gravity Bridge: started, bridge path: ${bridgePath}`); +} + +function stopBridge() { + if (!isActive) { return; } + + isActive = false; + statusBar.text = '$(radio-tower) Bridge: Off'; + statusBar.tooltip = 'Gravity Bridge — Click to start'; + statusBar.command = 'gravityBridge.start'; + + if (watcher) { + watcher.close(); + watcher = null; + } + + vscode.window.showInformationMessage('Gravity Bridge: Stopped'); +} + +/** + * Handle a response from Discord (approve/reject). + * Reads the response JSON and simulates the appropriate action. + */ +async function handleResponse(filePath: string) { + try { + // Small delay to ensure file is fully written + await new Promise(resolve => setTimeout(resolve, 200)); + + if (!fs.existsSync(filePath)) { return; } + + const content = fs.readFileSync(filePath, 'utf-8'); + const response = JSON.parse(content); + + if (response.approved === undefined) { return; } + + console.log(`Gravity Bridge: response received — approved=${response.approved}`); + + if (response.approved) { + // Simulate pressing Enter or clicking approve + // Strategy: Use VS Code command to accept suggestion + await simulateApproval(); + vscode.window.showInformationMessage(`✅ Approved: ${response.request_id}`); + } else { + await simulateRejection(); + vscode.window.showInformationMessage(`❌ Rejected: ${response.request_id}`); + } + + // Cleanup: delete the response file after processing + try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ } + + } catch (err) { + console.error('Gravity Bridge: error handling response', err); + } +} + +/** + * Handle a text command from Discord. + * Types the text into the active editor or chat input. + */ +async function handleCommand(filePath: string) { + try { + await new Promise(resolve => setTimeout(resolve, 200)); + + if (!fs.existsSync(filePath)) { return; } + + const content = fs.readFileSync(filePath, 'utf-8'); + const command = JSON.parse(content); + + if (command.consumed || !command.text) { return; } + + console.log(`Gravity Bridge: command received — "${command.text.substring(0, 50)}..."`); + + // Type into the active text input (chat panel) + await vscode.commands.executeCommand('workbench.action.chat.open'); + // Small delay for chat panel to open + await new Promise(resolve => setTimeout(resolve, 500)); + + // Type the text using clipboard + const oldClipboard = await vscode.env.clipboard.readText(); + await vscode.env.clipboard.writeText(command.text); + await vscode.commands.executeCommand('editor.action.clipboardPasteAction'); + await vscode.env.clipboard.writeText(oldClipboard); + + // Mark as consumed + command.consumed = true; + fs.writeFileSync(filePath, JSON.stringify(command, null, 2), 'utf-8'); + + vscode.window.showInformationMessage(`📨 Discord input: ${command.text.substring(0, 50)}...`); + } catch (err) { + console.error('Gravity Bridge: error handling command', err); + } +} + +/** + * Simulate approval — try multiple strategies. + */ +async function simulateApproval() { + try { + // Strategy 1: Try executing the accept command if available + await vscode.commands.executeCommand('workbench.action.acceptSelectedCodeAction'); + } catch { + // Strategy 2: Send Enter key via type command + try { + await vscode.commands.executeCommand('type', { text: '\n' }); + } catch { + // Strategy 3: Focus terminal and send Enter + await vscode.commands.executeCommand('workbench.action.terminal.focus'); + } + } +} + +/** + * Simulate rejection — try multiple strategies. + */ +async function simulateRejection() { + try { + // Strategy 1: Escape key + await vscode.commands.executeCommand('workbench.action.closeQuickOpen'); + } catch { + try { + await vscode.commands.executeCommand('cancelSelection'); + } catch { + // Fallback: just notify + console.log('Gravity Bridge: rejection sent but no active dialog found'); + } + } +} + +/** + * Manual approve/reject from command palette. + * Writes a pending request for testing purposes. + */ +function handleManualAction(approved: boolean) { + const requestId = `manual-${Date.now()}`; + const responsePath = path.join(bridgePath, 'response', `${requestId}.json`); + + const response = { + request_id: requestId, + approved: approved, + user_input: '', + timestamp: Date.now() / 1000, + }; + + fs.writeFileSync(responsePath, JSON.stringify(response, null, 2), 'utf-8'); + + if (approved) { + simulateApproval(); + } else { + simulateRejection(); + } +} + +/** + * Write a pending approval request to bridge/pending/ for Discord bot to pick up. + */ +export function writePendingApproval( + conversationId: string, + command: string, + description: string +): string { + const requestId = `req-${Date.now()}`; + const pendingPath = path.join(bridgePath, 'pending', `${requestId}.json`); + + const request = { + request_id: requestId, + conversation_id: conversationId, + command: command, + description: description, + timestamp: Date.now() / 1000, + status: 'pending', + discord_message_id: 0, + }; + + fs.writeFileSync(pendingPath, JSON.stringify(request, null, 2), 'utf-8'); + sentPendingIds.add(requestId); + + console.log(`Gravity Bridge: pending approval written — ${requestId}`); + return requestId; +} + +export function deactivate() { + stopBridge(); +} diff --git a/extension/tsconfig.json b/extension/tsconfig.json new file mode 100644 index 0000000..ca690b9 --- /dev/null +++ b/extension/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "outDir": "out", + "lib": [ + "ES2022" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file