├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml └── workflows │ └── inactive-issues.yml ├── ui-shell ├── bun.lockb ├── jsconfig.json ├── src │ ├── assets │ │ ├── style.css │ │ ├── tailwind-primevue-preset │ │ │ ├── avatargroup │ │ │ │ └── index.js │ │ │ ├── inputgroup │ │ │ │ └── index.js │ │ │ ├── ripple │ │ │ │ └── index.js │ │ │ ├── skeleton │ │ │ │ └── index.js │ │ │ ├── inputgroupaddon │ │ │ │ └── index.js │ │ │ ├── toolbar │ │ │ │ └── index.js │ │ │ ├── inputmask │ │ │ │ └── index.js │ │ │ ├── card │ │ │ │ └── index.js │ │ │ ├── dataview │ │ │ │ └── index.js │ │ │ ├── inlinemessage │ │ │ │ └── index.js │ │ │ ├── chip │ │ │ │ └── index.js │ │ │ ├── tooltip │ │ │ │ └── index.js │ │ │ ├── knob │ │ │ │ └── index.js │ │ │ ├── textarea │ │ │ │ └── index.js │ │ │ ├── tag │ │ │ │ └── index.js │ │ │ ├── breadcrumb │ │ │ │ └── index.js │ │ │ ├── overlaypanel │ │ │ │ └── index.js │ │ │ ├── terminal │ │ │ │ └── index.js │ │ │ ├── avatar │ │ │ │ └── index.js │ │ │ ├── scrolltop │ │ │ │ └── index.js │ │ │ ├── inputtext │ │ │ │ └── index.js │ │ │ ├── selectbutton │ │ │ │ └── index.js │ │ │ ├── badge │ │ │ │ └── index.js │ │ │ ├── badgedirective │ │ │ │ └── index.js │ │ │ ├── scrollpanel │ │ │ │ └── index.js │ │ │ ├── dataviewlayoutoptions │ │ │ │ └── index.js │ │ │ ├── global.js │ │ │ ├── progressbar │ │ │ │ └── index.js │ │ │ ├── rating │ │ │ │ └── index.js │ │ │ ├── password │ │ │ │ └── index.js │ │ │ ├── accordion │ │ │ │ └── index.js │ │ │ ├── togglebutton │ │ │ │ └── index.js │ │ │ ├── radiobutton │ │ │ │ └── index.js │ │ │ ├── message │ │ │ │ └── index.js │ │ │ ├── divider │ │ │ │ └── index.js │ │ │ ├── tabmenu │ │ │ │ └── index.js │ │ │ ├── inputswitch │ │ │ │ └── index.js │ │ │ ├── checkbox │ │ │ │ └── index.js │ │ │ ├── fieldset │ │ │ │ └── index.js │ │ │ ├── panel │ │ │ │ └── index.js │ │ │ ├── dock │ │ │ │ └── index.js │ │ │ ├── chips │ │ │ │ └── index.js │ │ │ ├── tristatecheckbox │ │ │ │ └── index.js │ │ │ ├── toast │ │ │ │ └── index.js │ │ │ ├── menu │ │ │ │ └── index.js │ │ │ └── tieredmenu │ │ │ │ └── index.js │ │ └── base.css │ ├── router │ │ └── index.js │ ├── stores │ │ └── counter.js │ ├── components │ │ └── __tests__ │ │ │ └── empty-state.cy.js │ └── main.js ├── public │ └── favicon.ico ├── postcss.config.js ├── cypress │ ├── fixtures │ │ └── example.json │ ├── e2e │ │ ├── jsconfig.json │ │ └── example.cy.js │ └── support │ │ ├── component-index.html │ │ ├── e2e.js │ │ ├── commands.js │ │ └── component.js ├── build.sh ├── vite.config.js ├── index.html ├── cypress.config.js ├── package.json ├── README.md └── tailwind.config.js ├── resources ├── devdb.png ├── devdb.xcf ├── devdb-128x128.png ├── featured │ ├── ddev.png │ ├── daily-dev.png │ ├── laravel-new.png │ └── test-dev-tools.png ├── screenshots │ ├── devdb.gif │ ├── devdb.png │ ├── main-view.png │ ├── devdb-dad-joke.png │ ├── new │ │ ├── main-dark.png │ │ ├── main-light.png │ │ ├── mcp-setup.png │ │ ├── mcp-usage.png │ │ ├── providers.png │ │ ├── mysql-explain.png │ │ ├── main-light-dark.png │ │ ├── main-light-dark.xcf │ │ ├── providers-dark.png │ │ ├── ui-actions-preview.png │ │ ├── providers-light-dark.png │ │ ├── providers-light-dark.xcf │ │ ├── context-menu-contributions.png │ │ └── laravel-eloquent-code-lens.png │ ├── laravel-code-lens.png │ ├── light-dark-mode.png │ ├── light-dark-mode.xcf │ ├── sample-view-location.png │ ├── light-dark-mode-with-qr.png │ ├── light-dark-mode-with-qr.xcf │ ├── context-menu-contribution.png │ └── sample-view-location-plain.png ├── light-dark-mode-with-qr.xcf └── devdb-marketplace-url-qr-code.png ├── apps ├── rails │ ├── todo-app │ │ └── .gitignore │ └── README.md └── django │ ├── todo-app │ └── .gitignore │ └── README.md ├── CHANGELOG.md ├── src ├── services │ ├── mcp │ │ └── no-vscode │ │ │ ├── config.ts │ │ │ ├── README.md │ │ │ ├── logger.ts │ │ │ └── port-manager.ts │ ├── initialization-error-service.ts │ ├── random-string-generator.ts │ ├── logging-service.ts │ ├── document-service.ts │ ├── error-notification-service.ts │ ├── codelens │ │ └── code-lens-service.ts │ ├── output-service.ts │ ├── laravel │ │ ├── code-runner │ │ │ └── qualifier-service.ts │ │ ├── sail.ts │ │ ├── artisan-service.ts │ │ └── laravel-core.ts │ ├── pagination.ts │ ├── config-service.ts │ ├── connector.ts │ ├── context-menu-service.ts │ ├── string.ts │ ├── workspace.ts │ ├── go-to-table.ts │ ├── adonis │ │ ├── env-file-parser.ts │ │ └── adonis-core.ts │ └── html.ts ├── constants.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── index.ts │ │ └── services │ │ ├── string.test.ts │ │ ├── qualifier-service.test.ts │ │ └── pagination.test.ts ├── providers │ ├── postgres │ │ ├── laravel-postgres-provider.ts │ │ ├── ddev-postgres-provider.ts │ │ ├── rails-postgres-provider.ts │ │ ├── django-postgres-provider.ts │ │ └── adonis-postgres-provider.ts │ ├── sqlite │ │ ├── file-picker-sqlite-provider.ts │ │ ├── laravel-local-sqlite-provider.ts │ │ └── rails-sqlite-provider.ts │ └── mysql │ │ ├── ddev-mysql-provider.ts │ │ ├── laravel-mysql-provider.ts │ │ ├── adonis-mysql-provider.ts │ │ ├── rails-mysql-provider.ts │ │ └── django-mysql-provider.ts └── uri-handler.ts ├── scripts ├── showcase.sh └── triage │ ├── sqlite-triage.sh │ ├── mysql-triage.sh │ └── postgres-triage.sh ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── .vscodeignore ├── .eslintrc.json ├── tsconfig.json ├── LICENSE.txt ├── .gitignore ├── esbuild.js └── snippets └── devdbrc.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: damms005 2 | -------------------------------------------------------------------------------- /ui-shell/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/ui-shell/bun.lockb -------------------------------------------------------------------------------- /ui-shell/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/devdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/devdb.png -------------------------------------------------------------------------------- /resources/devdb.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/devdb.xcf -------------------------------------------------------------------------------- /ui-shell/src/assets/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /resources/devdb-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/devdb-128x128.png -------------------------------------------------------------------------------- /resources/featured/ddev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/featured/ddev.png -------------------------------------------------------------------------------- /ui-shell/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/ui-shell/public/favicon.ico -------------------------------------------------------------------------------- /resources/featured/daily-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/featured/daily-dev.png -------------------------------------------------------------------------------- /resources/screenshots/devdb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/devdb.gif -------------------------------------------------------------------------------- /resources/screenshots/devdb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/devdb.png -------------------------------------------------------------------------------- /resources/featured/laravel-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/featured/laravel-new.png -------------------------------------------------------------------------------- /resources/screenshots/main-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/main-view.png -------------------------------------------------------------------------------- /resources/featured/test-dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/featured/test-dev-tools.png -------------------------------------------------------------------------------- /resources/light-dark-mode-with-qr.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/light-dark-mode-with-qr.xcf -------------------------------------------------------------------------------- /ui-shell/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /resources/screenshots/devdb-dad-joke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/devdb-dad-joke.png -------------------------------------------------------------------------------- /resources/screenshots/new/main-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/main-dark.png -------------------------------------------------------------------------------- /resources/screenshots/new/main-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/main-light.png -------------------------------------------------------------------------------- /resources/screenshots/new/mcp-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/mcp-setup.png -------------------------------------------------------------------------------- /resources/screenshots/new/mcp-usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/mcp-usage.png -------------------------------------------------------------------------------- /resources/screenshots/new/providers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/providers.png -------------------------------------------------------------------------------- /resources/devdb-marketplace-url-qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/devdb-marketplace-url-qr-code.png -------------------------------------------------------------------------------- /resources/screenshots/laravel-code-lens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/laravel-code-lens.png -------------------------------------------------------------------------------- /resources/screenshots/light-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/light-dark-mode.png -------------------------------------------------------------------------------- /resources/screenshots/light-dark-mode.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/light-dark-mode.xcf -------------------------------------------------------------------------------- /resources/screenshots/new/mysql-explain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/mysql-explain.png -------------------------------------------------------------------------------- /resources/screenshots/new/main-light-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/main-light-dark.png -------------------------------------------------------------------------------- /resources/screenshots/new/main-light-dark.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/main-light-dark.xcf -------------------------------------------------------------------------------- /resources/screenshots/new/providers-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/providers-dark.png -------------------------------------------------------------------------------- /resources/screenshots/new/ui-actions-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/ui-actions-preview.png -------------------------------------------------------------------------------- /resources/screenshots/sample-view-location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/sample-view-location.png -------------------------------------------------------------------------------- /resources/screenshots/light-dark-mode-with-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/light-dark-mode-with-qr.png -------------------------------------------------------------------------------- /resources/screenshots/light-dark-mode-with-qr.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/light-dark-mode-with-qr.xcf -------------------------------------------------------------------------------- /resources/screenshots/new/providers-light-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/providers-light-dark.png -------------------------------------------------------------------------------- /resources/screenshots/new/providers-light-dark.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/providers-light-dark.xcf -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/avatargroup/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: 'flex items-center' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /resources/screenshots/context-menu-contribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/context-menu-contribution.png -------------------------------------------------------------------------------- /resources/screenshots/sample-view-location-plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/sample-view-location-plain.png -------------------------------------------------------------------------------- /apps/rails/todo-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this ephemeral Rails application directory 2 | * 3 | # But don't ignore this .gitignore file itself 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /resources/screenshots/new/context-menu-contributions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/context-menu-contributions.png -------------------------------------------------------------------------------- /resources/screenshots/new/laravel-eloquent-code-lens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damms005/devdb-vscode/HEAD/resources/screenshots/new/laravel-eloquent-code-lens.png -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inputgroup/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: ['flex items-stretch', 'w-full'] 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /apps/django/todo-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this ephemeral Django application directory 2 | * 3 | # But don't ignore this .gitignore file itself 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /ui-shell/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hey@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /ui-shell/cypress/e2e/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress"] 6 | }, 7 | "include": ["./**/*", "../support/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /ui-shell/cypress/e2e/example.cy.js: -------------------------------------------------------------------------------- 1 | // https://on.cypress.io/api 2 | 3 | describe('My First Test', () => { 4 | it('visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'You did it!') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature request 4 | url: https://github.com/damms005/devdb-vscode/discussions/new?category=ideas 5 | about: Share ideas for new features 6 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/ripple/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: ['block absolute bg-surface-0/50 rounded-full pointer-events-none'], 4 | style: 'transform: scale(0)' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | We try not to use 'WIP' commit messages. Hence, a commit log with descriptive messages is the best CHANGELOG we can offer for now. 2 | 3 | [Click here for full commit log](https://github.com/damms005/devdb-vscode/commits/main) 4 | -------------------------------------------------------------------------------- /src/services/mcp/no-vscode/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as os from 'os'; 3 | 4 | export const MCP_CONFIG_DIR = path.join(os.homedir(), '.devdb'); 5 | export const MCP_CONFIG_FILE = path.join(MCP_CONFIG_DIR, 'mcp.json'); 6 | -------------------------------------------------------------------------------- /scripts/showcase.sh: -------------------------------------------------------------------------------- 1 | # Script to showcase new DevDb features. It opens a new VS Code instance 2 | # with a Profile that is preset to suit making DevDb screenshots and screen casts 3 | code --profile "DevDb screenshot" '/Users/damms005/Herd/nft-marketplace' -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "connor4312.esbuild-problem-matchers" 7 | ] 8 | } -------------------------------------------------------------------------------- /ui-shell/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | const router = createRouter({ 4 | history: createWebHistory(import.meta.env.BASE_URL), 5 | routes: [ 6 | ] 7 | }) 8 | 9 | export default router 10 | -------------------------------------------------------------------------------- /ui-shell/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'Building Vue assets' 3 | pwd 4 | npm run build 5 | cd .. 6 | pwd 7 | echo 'Compiling extension code' 8 | npm run compile 9 | cd - 10 | echo 'Back to Vue project folder' 11 | pwd 12 | echo "Completed at $(date +"%Y-%m-%d %I:%M:%S%p")" 13 | -------------------------------------------------------------------------------- /src/services/initialization-error-service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * When running tests, the vscode module is not available. 3 | * So we here to prevent "Cannot find module 'vscode'" error. 4 | */ 5 | export async function reportError(error: string) { 6 | const vscode = await require('vscode'); 7 | vscode.window.showErrorMessage(`${String(error)}`) 8 | } -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const enum ExtensionConstants { 2 | extensionId = "damms005.devdb", 3 | globalVersionKey = "devdb-version", 4 | clickedGitHubStarring = "clicked-on-devdb-github-repo-starring", 5 | clickedToFollowOnX = "clicked-to-follow-on-x", 6 | clickedToSupport = "clicked-to-support", 7 | clickedToSponsor = "clicked-to-sponsor", 8 | } 9 | -------------------------------------------------------------------------------- /ui-shell/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /ui-shell/src/stores/counter.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counter', () => { 5 | const count = ref(0) 6 | const doubleCount = computed(() => count.value * 2) 7 | function increment() { 8 | count.value++ 9 | } 10 | 11 | return { count, doubleCount, increment } 12 | }) 13 | -------------------------------------------------------------------------------- /ui-shell/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | "@": fileURLToPath(new URL("./src", import.meta.url)), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /ui-shell/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui-shell/src/components/__tests__/empty-state.cy.js: -------------------------------------------------------------------------------- 1 | import Initializer from '../Initializer.vue' 2 | 3 | describe('Initializer', () => { 4 | it('playground', () => { 5 | cy.mount(Initializer, { props: { msg: 'Hey!' } }) 6 | }) 7 | 8 | it('renders properly', () => { 9 | cy.mount(Initializer, { props: { msg: 'Hey!' } }) 10 | cy.get('h1').should('contain', 'Hey!') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /ui-shell/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', 6 | baseUrl: 'http://localhost:4173' 7 | }, 8 | component: { 9 | specPattern: 'src/**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}', 10 | devServer: { 11 | framework: 'vue', 12 | bundler: 'vite' 13 | } 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/services/random-string-generator.ts: -------------------------------------------------------------------------------- 1 | export function getRandomString(prefix = '', length = 8): string { 2 | let result = `${Date.now()}-` 3 | 4 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 5 | const charactersLength = characters.length 6 | for (let i = 0; i < length; i++) { 7 | result += characters.charAt(Math.floor(Math.random() * charactersLength)) 8 | } 9 | 10 | return `${prefix}${result}` 11 | } -------------------------------------------------------------------------------- /ui-shell/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import PrimeVue from 'primevue/config' 4 | import Lara from './assets/tailwind-primevue-preset' 5 | import App from './App.vue' 6 | import router from './router' 7 | 8 | const app = createApp(App) 9 | 10 | app.use(createPinia()) 11 | app.use(router) 12 | 13 | app.use(PrimeVue, { 14 | unstyled: true, 15 | pt: Lara, 16 | }) 17 | 18 | app.mount('#app') 19 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # Ignore everything by default 2 | * 3 | ** 4 | 5 | # Include only necessary files 6 | !package.json 7 | !LICENSE* 8 | 9 | # Include source and compiled files 10 | !*.js 11 | !dist/**/*.js 12 | !ui-shell/dist/** 13 | 14 | # Include icons 15 | !resources/devdb-128x128.png 16 | !resources/devdb.png 17 | 18 | # Include snippets and schemas 19 | !snippets/* 20 | !schemas/* 21 | 22 | # Include @vscode/sqlite3 because we excluded it from build in esbuild.js 23 | !node_modules/@vscode/sqlite3/** -------------------------------------------------------------------------------- /src/services/logging-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { logToOutput } from './output-service'; 3 | 4 | export function log(description: string, message: string, ...rest: any[]) { 5 | const config = vscode.workspace.getConfiguration('Devdb'); 6 | const showDebugInfo = config.get('showDebugInfo', false); 7 | 8 | if (showDebugInfo) { 9 | console.log(message, ...rest); 10 | } 11 | 12 | logToOutput(`${message} ${rest.map(String).join(' ').trim()}`, description) 13 | } 14 | -------------------------------------------------------------------------------- /src/services/document-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Gets the word under the cursor in the 5 | * current document 6 | */ 7 | export function getWordUnderCursor(): string | undefined { 8 | const editor = vscode.window.activeTextEditor; 9 | if (!editor) return; 10 | 11 | const document = editor.document; 12 | const cursorPosition = editor.selection.active; 13 | const wordRange = document.getWordRangeAtPosition(cursorPosition); 14 | 15 | return document.getText(wordRange) 16 | } -------------------------------------------------------------------------------- /src/services/mcp/no-vscode/README.md: -------------------------------------------------------------------------------- 1 | Modules in this folder should be self-contained and not access any VS Code APIs 2 | 3 | This is because they contain dependencies that are consumed directly by the external MCP server. The MCP server runs in its own process space, not having access to VS Code APIs which are created at runtime. 4 | 5 | Also, since we are using STDIO transport, no writing to stdio from our script, e.g. using console.log, etc. See https://modelcontextprotocol.io/docs/develop/build-server#logging-in-mcp-servers 6 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/skeleton/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | 'overflow-hidden', 5 | { 6 | 'animate-pulse': props.animation !== 'none' 7 | }, 8 | 9 | // Round 10 | { 'rounded-full': props.shape === 'circle', 'rounded-md': props.shape !== 'circle' }, 11 | 12 | // Colors 13 | 'bg-surface-200 dark:bg-surface-700' 14 | ] 15 | }) 16 | }; 17 | -------------------------------------------------------------------------------- /src/services/mcp/no-vscode/logger.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as winston from 'winston'; 3 | import { MCP_CONFIG_DIR } from './config'; 4 | 5 | const logger = winston.createLogger({ 6 | level: 'info', 7 | format: winston.format.combine( 8 | winston.format.timestamp(), 9 | winston.format.errors({ stack: true }), 10 | winston.format.json() 11 | ), 12 | defaultMeta: { service: 'devdb-mcp-server' }, 13 | transports: [ 14 | new winston.transports.File({ filename: path.join(MCP_CONFIG_DIR, 'mcp-log.txt') }), 15 | ], 16 | }); 17 | 18 | 19 | export default logger -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "off", 13 | "@typescript-eslint/semi": "off", 14 | "curly": "off", 15 | "no-throw-literal": "warn", 16 | "semi": "off" 17 | }, 18 | "ignorePatterns": [ 19 | "out", 20 | "dist", 21 | "**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/services/error-notification-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function showMissingDatabaseNotification() { 4 | vscode.window.showErrorMessage('No database connection found. Please select a database in DevDb and try again.', 'Connect').then(selection => { 5 | if (selection === 'Connect') { 6 | vscode.commands.executeCommand('devdb.focus'); 7 | } 8 | }); 9 | } 10 | 11 | export function showEmptyTablesNotification() { 12 | vscode.window.showErrorMessage('Tables not loaded yet. Please ensure to connect to a database in DevDb and try again.', 'Connect').then(selection => { 13 | if (selection === 'Connect') { 14 | vscode.commands.executeCommand('devdb.focus'); 15 | } 16 | }); 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "**/ui-shell/**" 4 | ], 5 | "compilerOptions": { 6 | "module": "Node16", 7 | "target": "ES2022", 8 | "outDir": "out", 9 | "lib": [ 10 | "ES2022" 11 | ], 12 | "allowImportingTsExtensions": false, 13 | "sourceMap": true, 14 | "rootDir": "src", 15 | "strict": true /* enable all strict type-checking options */, 16 | "esModuleInterop": true, 17 | /* Additional Checks */ 18 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 19 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 20 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 21 | } 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.formatOnSave": true, 4 | "files.exclude": { 5 | "out": false // set this to true to hide the "out" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true // set this to false to include "out" folder in search results 9 | }, 10 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 11 | "typescript.tsc.autoDetect": "off", 12 | "editor.defaultFormatter": "esbenp.prettier-vscode", 13 | "cSpell.words": [ 14 | "devdb", 15 | "devdbrc", 16 | "notnull", 17 | "snakecase", 18 | "testcontainers" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests', err); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /ui-shell/src/assets/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-50: 236 253 245; 3 | --primary-100: 209 250 229; 4 | --primary-200: 167 243 208; 5 | --primary-300: 110 231 183; 6 | --primary-400: 52 211 153; 7 | --primary-500: 16 185 129; 8 | --primary-600: 5 150 105; 9 | --primary-700: 4 120 87; 10 | --primary-800: 6 95 70; 11 | --primary-900: 4 78 56; 12 | --primary-950: 2 44 34; 13 | --surface-0: 255 255 255; 14 | --surface-50: 248 250 252; 15 | --surface-100: 241 245 249; 16 | --surface-200: 226 232 240; 17 | --surface-300: 203 213 225; 18 | --surface-400: 148 163 184; 19 | --surface-500: 100 116 139; 20 | --surface-600: 71 85 105; 21 | --surface-700: 45 55 72; 22 | --surface-800: 30 41 59; 23 | --surface-900: 15 23 42; 24 | --surface-950: 3 6 23; 25 | } -------------------------------------------------------------------------------- /ui-shell/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inputgroupaddon/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Flex 5 | 'flex items-center justify-center', 6 | 7 | // Shape 8 | 'first:rounded-l-md', 9 | 'last:rounded-r-md', 10 | 'border-y', 11 | 12 | 'last:border-r', 13 | 'border-l', 14 | 'border-r-0', 15 | 16 | // Space 17 | 'p-1.5', 18 | 19 | // Size 20 | 'min-w-[3rem]', 21 | 22 | // Color 23 | 'bg-surface-50 dark:bg-surface-800', 24 | 'text-surface-600 dark:text-surface-400', 25 | 'border-surface-300 dark:border-surface-600' 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/toolbar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Flex & Alignment 5 | 'flex items-center justify-between flex-wrap', 6 | 'gap-6', 7 | 8 | // Spacing 9 | 'px-6 py-2', 10 | 'min-h-[4rem]', 11 | 12 | // Shape 13 | 'rounded-md', 14 | 'shadow-md', 15 | 16 | // Color 17 | 'bg-surface-0 dark:bg-surface-900', 18 | 'ring-1 ring-surface-100 dark:ring-surface-700' 19 | ] 20 | }, 21 | start: { 22 | class: 'flex items-center' 23 | }, 24 | center: { 25 | class: 'flex items-center' 26 | }, 27 | end: { 28 | class: 'flex items-center' 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/inactive-issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "1 1 1,14 * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - uses: actions/stale@v5 13 | with: 14 | any-of-labels: needs more info,awaiting response 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 14 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity and the 'awaiting response' tag persists. Please note that it will be auto-closed soon if there is still no response. Thank you." 19 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /src/services/codelens/code-lens-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LaravelCodelensService } from './laravel/laravel-codelens-service'; 3 | 4 | export class LaravelCodelensProvider implements vscode.CodeLensProvider { 5 | 6 | private codeLenses: vscode.CodeLens[] = []; 7 | private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); 8 | public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event; 9 | 10 | constructor() { } 11 | 12 | public async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { 13 | this.codeLenses = []; 14 | 15 | const laravelCodeLenses = await LaravelCodelensService.getCodelensFor(document) 16 | 17 | if (laravelCodeLenses) { 18 | this.codeLenses = laravelCodeLenses 19 | } 20 | 21 | return this.codeLenses; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "watch", 6 | "dependsOn": [ 7 | "npm: watch:tsc", 8 | "npm: watch:esbuild" 9 | ], 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch:esbuild", 21 | "group": "build", 22 | "problemMatcher": "$esbuild-watch", 23 | "isBackground": true, 24 | "label": "npm: watch:esbuild", 25 | "presentation": { 26 | "group": "watch", 27 | "reveal": "never" 28 | } 29 | }, 30 | { 31 | "type": "npm", 32 | "script": "watch:tsc", 33 | "group": "build", 34 | "problemMatcher": "$tsc-watch", 35 | "isBackground": true, 36 | "label": "npm: watch:tsc", 37 | "presentation": { 38 | "group": "watch", 39 | "reveal": "never" 40 | } 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /ui-shell/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /src/services/output-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | let outputChannel: vscode.OutputChannel | undefined; 4 | 5 | /** 6 | * Logs a message to the DevDb output channel 7 | */ 8 | export function logToOutput(message: string, description = ''): void { 9 | const channel = getOutputChannel(); 10 | const formattedMessage = `${description ? `[${description}]` : ''} ${message}`; 11 | channel.appendLine(formattedMessage); 12 | } 13 | 14 | export function clearOutput(): void { 15 | const channel = getOutputChannel(); 16 | channel.clear(); 17 | } 18 | 19 | /** 20 | * Shows the DevDb output channel in the VS Code UI 21 | */ 22 | export function showOutput(): void { 23 | const channel = getOutputChannel(); 24 | channel.show(); 25 | } 26 | 27 | function getOutputChannel(): vscode.OutputChannel { 28 | if (!outputChannel) { 29 | outputChannel = vscode.window.createOutputChannel("DevDb"); 30 | } 31 | return outputChannel; 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "--disable-extensions" 15 | ], 16 | "preLaunchTask": "${defaultBuildTask}" 17 | }, 18 | { 19 | "name": "Extension Tests", 20 | "type": "extensionHost", 21 | "request": "launch", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", 25 | "--disable-extensions" 26 | ], 27 | "preLaunchTask": "${defaultBuildTask}" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /ui-shell/cypress/support/component.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | // Import global styles 23 | import '@/assets/main.css' 24 | 25 | import { mount } from 'cypress/vue' 26 | 27 | Cypress.Commands.add('mount', mount) 28 | 29 | // Example use: 30 | // cy.mount(MyComponent) 31 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inputmask/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ context }) => ({ 3 | class: [ 4 | // Font 5 | 'font-sans leading-6', 6 | 7 | // Spacing 8 | 'm-0 py-1.5 px-3 sm:text-sm', 9 | 10 | // Colors 11 | 'text-surface-900 dark:text-surface-0', 12 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500', 13 | 'bg-surface-0 dark:bg-surface-900', 14 | 'ring-1 ring-inset ring-surface-300 dark:ring-surface-700 ring-offset-0', 15 | 'shadow-sm', 16 | 17 | // Shape 18 | 'rounded-md', 19 | 'appearance-none', 20 | 21 | // Interactions 22 | { 23 | 'outline-none focus:ring-primary-500 dark:focus:ring-primary-400': !context.disabled, 24 | 'opacity-60 select-none pointer-events-none cursor-default': context.disabled 25 | } 26 | ] 27 | }) 28 | }; 29 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/card/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | //Shape 5 | 'rounded-lg', 6 | 'shadow-md', 7 | 8 | //Color 9 | 'bg-surface-0 dark:bg-surface-900', 10 | 'text-surface-700 dark:text-surface-0/80' 11 | ] 12 | }, 13 | header: { 14 | class: ['border-b border-surface-200 dark:border-surface-700'] 15 | }, 16 | body: { 17 | class: 'py-5' 18 | }, 19 | title: { 20 | class: 'text-lg font-medium mb-2 px-5 md:px-6' 21 | }, 22 | subtitle: { 23 | class: [ 24 | //Spacing 25 | 'mb-1 px-5 md:px-6', 26 | 27 | //Color 28 | 'text-surface-600 dark:text-surface-0/60' 29 | ] 30 | }, 31 | content: { 32 | class: 'py-6 px-5 md:px-6' 33 | }, 34 | footer: { 35 | class: ['px-5 md:px-6 pt-5 pb-0', 'border-t border-surface-200 dark:border-surface-700'] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/dataview/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | content: { 3 | class: [ 4 | // Spacing 5 | 'p-0', 6 | 7 | // Shape 8 | 'border-0', 9 | 10 | // Color 11 | 'text-surface-700 dark:text-white/80', 12 | 'bg-surface-0 dark:bg-surface-800' 13 | ] 14 | }, 15 | grid: { 16 | class: [ 17 | // flex 18 | 'flex flex-wrap', 19 | 20 | // Spacing 21 | 'ml-0 mr-0 mt-0', 22 | 23 | // Color 24 | 'bg-surface-0 dark:bg-surface-800' 25 | ] 26 | }, 27 | header: { 28 | class: [ 29 | 'font-semibold', 30 | 31 | // Spacing 32 | 'p-6', 33 | 34 | // Color 35 | 'text-surface-800 dark:text-white/80', 36 | 'bg-surface-0 dark:bg-surface-800', 37 | 'border-surface-200 dark:border-surface-700 border-b' 38 | ] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | const testFiles = new glob.Glob("**/**.test.js", { cwd: testsRoot }); 16 | const testFileStream = testFiles.stream(); 17 | 18 | testFileStream.on("data", (file) => { 19 | mocha.addFile(path.resolve(testsRoot, file)); 20 | }); 21 | testFileStream.on("error", (err) => { 22 | e(err); 23 | }); 24 | testFileStream.on("end", () => { 25 | try { 26 | // Run the mocha test 27 | mocha.run(failures => { 28 | if (failures > 0) { 29 | e(new Error(`${failures} tests failed.`)); 30 | } else { 31 | c(); 32 | } 33 | }); 34 | } catch (err) { 35 | console.error(err); 36 | e(err); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inlinemessage/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | 'inline-flex items-center justify-center align-top gap-2', 5 | 'py-2 px-3 m-0 rounded-md', 6 | 'ring-1 ring-inset ring-surface-200 dark:ring-surface-700 ring-offset-0', 7 | { 8 | 'text-blue-500 dark:text-blue-300': props.severity == 'info', 9 | 'text-green-500 dark:text-green-300': props.severity == 'success', 10 | 'text-orange-500 dark:text-orange-300': props.severity == 'warn', 11 | 'text-red-500 dark:text-red-300': props.severity == 'error' 12 | } 13 | ] 14 | }), 15 | icon: { 16 | class: [ 17 | // Sizing and Spacing 18 | 'w-4 h-4', 19 | 'shrink-0' 20 | ] 21 | }, 22 | text: { 23 | class: [ 24 | // Font and Text 25 | 'text-sm leading-none', 26 | 'font-medium' 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/chip/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Flexbox 5 | 'inline-flex items-center', 6 | 7 | // Spacing 8 | 'px-2 py-0.5', 9 | 10 | // Shape 11 | 'rounded-[1.14rem]', 12 | 13 | // Colors 14 | 'text-surface-700 dark:text-surface-0/70', 15 | 'bg-surface-200 dark:bg-surface-700' 16 | ] 17 | }, 18 | label: { 19 | class: 'text-xs leading-6 mx-0' 20 | }, 21 | icon: { 22 | class: 'leading-6 mr-2' 23 | }, 24 | image: { 25 | class: ['w-6 h-6 mr-2', 'rounded-full'] 26 | }, 27 | removeIcon: { 28 | class: [ 29 | // Shape 30 | 'rounded-md leading-6', 31 | 32 | // Spacing 33 | 'ml-2', 34 | 35 | // Size 36 | 'w-4 h-4', 37 | 38 | // Transition 39 | 'transition duration-200 ease-in-out', 40 | 41 | // Misc 42 | 'cursor-pointer' 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/tooltip/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ context, props }) => ({ 3 | class: [ 4 | // Position 5 | 'absolute', 6 | // Spacing 7 | { 8 | 'px-1.5': context?.right || context?.left || (!context?.right && !context?.left && !context?.top && !context?.bottom), 9 | 'py-1.5': context?.top || context?.bottom 10 | } 11 | ] 12 | }), 13 | arrow: { 14 | class: 'hidden' 15 | }, 16 | text: { 17 | class: [ 18 | // Size 19 | 'text-xs leading-none', 20 | 21 | // Spacing 22 | 'p-2', 23 | 24 | // Shape 25 | 'rounded-md', 26 | 27 | // Color 28 | 'text-surface-900 dark:text-surface-0/80', 29 | 'bg-surface-0 dark:bg-surface-900', 30 | 'ring-1 ring-inset ring-surface-200 dark:ring-surface-800 ring-offset-0', 31 | 32 | // Misc 33 | 'whitespace-pre-line', 34 | 'break-words' 35 | ] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/knob/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Misc 5 | { 'opacity-60 select-none pointer-events-none cursor-default': props.disabled } 6 | ] 7 | }), 8 | range: { 9 | class: [ 10 | // Stroke 11 | 'stroke-current', 12 | 13 | // Color 14 | 'stroke-surface-200 dark:stroke-surface-700', 15 | 16 | // Fill 17 | 'fill-none', 18 | 19 | // Transition 20 | 'transition duration-100 ease-in' 21 | ] 22 | }, 23 | value: { 24 | class: [ 25 | // Animation 26 | 'animate-dash-frame', 27 | 28 | // Color 29 | 'stroke-primary-500 dark:stroke-primary-400', 30 | 31 | // Fill 32 | 'fill-none' 33 | ] 34 | }, 35 | label: { 36 | class: [ 37 | // Text Style 38 | 'text-center text-xl', 39 | 40 | // Color 41 | 'fill-surface-600 dark:fill-surface-200' 42 | ] 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /ui-shell/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "watch": "chokidar -s '../src/**/*.ts' './src/**/*.{vue,js,ts,css}' './node_modules/devdb-ui/dist/**/*.*' -c './build.sh'", 9 | "preview": "vite preview", 10 | "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", 11 | "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", 12 | "test:unit": "cypress run --component", 13 | "test:unit:dev": "cypress open --component" 14 | }, 15 | "dependencies": { 16 | "devdb-ui": "file:../../devdb-ui", 17 | "pinia": "^2.3.1", 18 | "primevue": "^3.53.1", 19 | "vue": "^3.5.22", 20 | "vue-router": "^4.6.3" 21 | }, 22 | "devDependencies": { 23 | "@vitejs/plugin-vue": "^4.6.2", 24 | "autoprefixer": "^10.4.21", 25 | "chokidar-cli": "^3.0.0", 26 | "cypress": "^13.17.0", 27 | "postcss": "^8.5.6", 28 | "start-server-and-test": "^2.1.2", 29 | "tailwindcss": "^3.4.18", 30 | "vite": "^7.1.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Damilola Emmanuel Olowookere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/textarea/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ context }) => ({ 3 | class: [ 4 | // Font 5 | 'font-sans leading-6', 6 | 'sm:text-sm', 7 | 8 | // Spacing 9 | 'm-0', 10 | 'py-1.5 px-3', 11 | 12 | // Shape 13 | 'rounded-md', 14 | 'appearance-none', 15 | 16 | // Colors 17 | 'text-surface-900 dark:text-surface-0', 18 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500', 19 | 'bg-surface-0 dark:bg-surface-900', 20 | 'ring-1 ring-inset ring-surface-300 dark:ring-surface-700 ring-offset-0', 21 | 'shadow-sm', 22 | 23 | // States 24 | { 25 | 'outline-none focus:ring-primary-500 dark:focus:ring-primary-400': !context.disabled, 26 | 'opacity-60 select-none pointer-events-none cursor-default': context.disabled 27 | }, 28 | 29 | // Misc 30 | 'appearance-none', 31 | 'transition-colors duration-200' 32 | ] 33 | }) 34 | }; 35 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/tag/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | //Font 5 | 'text-xs font-bold', 6 | 7 | //Alignments 8 | 'inline-flex items-center justify-center', 9 | 10 | //Spacing 11 | 'px-2 py-1', 12 | 13 | //Shape 14 | { 15 | 'rounded-md': !props.rounded, 16 | 'rounded-full': props.rounded 17 | }, 18 | 19 | //Colors 20 | 'text-white dark:text-surface-900', 21 | { 22 | 'bg-primary-500 dark:bg-primary-400': props.severity == null || props.severity == 'primary', 23 | 'bg-green-500 dark:bg-green-400': props.severity == 'success', 24 | 'bg-blue-500 dark:bg-blue-400': props.severity == 'info', 25 | 'bg-orange-500 dark:bg-orange-400': props.severity == 'warning', 26 | 'bg-red-500 dark:bg-red-400': props.severity == 'danger' 27 | } 28 | ] 29 | }), 30 | value: { 31 | class: 'leading-normal' 32 | }, 33 | icon: { 34 | class: 'mr-1 text-sm' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/services/laravel/code-runner/qualifier-service.ts: -------------------------------------------------------------------------------- 1 | import { Engine, Program } from 'php-parser'; 2 | 3 | export function getAst(code: string): Program { 4 | const parser = new Engine({ 5 | parser: { 6 | php7: true 7 | }, 8 | ast: { 9 | withPositions: true 10 | } 11 | }); 12 | 13 | return parser.parseCode(code, 'file.php'); 14 | } 15 | 16 | export function extractUseStatements(ast: Program): string { 17 | let useStatements = ''; 18 | 19 | if (ast.children) { 20 | ast.children.forEach((node: any) => { 21 | if (node.kind === 'namespace' && node.name) { 22 | if (node.children) { 23 | node.children.forEach((childNode: any) => { 24 | if (childNode.kind === 'usegroup') { 25 | childNode.items.forEach((useItem: any) => { 26 | const alias = useItem.alias ? ` as ${getAlias(useItem.alias)}` : ''; 27 | useStatements += `use ${useItem.name}${alias};\n`; 28 | }); 29 | } 30 | }); 31 | } 32 | } 33 | }); 34 | } 35 | 36 | return useStatements; 37 | } 38 | 39 | function getAlias(alias: any) { 40 | return typeof alias === 'object' 41 | ? `${alias.name}` 42 | : alias; 43 | } 44 | 45 | export function isNamespaced(ast: Program): boolean { 46 | return ast.children.some((node: any) => node.kind === 'namespace'); 47 | } -------------------------------------------------------------------------------- /scripts/triage/sqlite-triage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Compute the script directory and set the database file path 4 | DB_PATH="$(dirname "$(readlink -f "$0")")/devdb-triage.sqlite" 5 | 6 | # Create a SQLite database file 7 | sqlite3 "$DB_PATH" << EOF 8 | CREATE TABLE book ( 9 | id INTEGER PRIMARY KEY AUTOINCREMENT, 10 | title TEXT NOT NULL, 11 | author TEXT NOT NULL, 12 | published_date DATE, 13 | isbn TEXT, 14 | pages INTEGER, 15 | available BOOLEAN DEFAULT 1 16 | ); 17 | INSERT INTO book (title, author, published_date, isbn, pages, available) VALUES 18 | ('The Great Gatsby', 'F. Scott Fitzgerald', '1925-04-10', '9780743273565', 180, 1), 19 | ('1984', 'George Orwell', '1949-06-08', '9780451524935', 328, 0), 20 | ('To Kill a Mockingbird', 'Harper Lee', '1960-07-11', '9780061120084', 281, 1), 21 | ('Pride and Prejudice', 'Jane Austen', '1813-01-28', '9781503290563', 279, 1), 22 | ('The Catcher in the Rye', 'J.D. Salinger', '1951-07-16', '9780316769488', 214, 0); 23 | 24 | EOF 25 | 26 | echo "SQLite triage setup complete." 27 | 28 | echo "Example connection details:" 29 | 30 | cat << EXAMPLE_CONNECTION 31 | { 32 | "database": "$DB_PATH" 33 | } 34 | EXAMPLE_CONNECTION 35 | 36 | echo "When done, remove the SQLite database file by runing:" 37 | echo "rm \"$DB_PATH\"" 38 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/breadcrumb/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: { 3 | class: [ 4 | // Flex & Alignment 5 | 'flex items-center flex-nowrap gap-x-1.5', 6 | 7 | // Spacing 8 | 'm-0 p-0 list-none leading-none' 9 | ] 10 | }, 11 | action: { 12 | class: [ 13 | // Font 14 | 'font-semibold text-decoration-none text-sm', 15 | 16 | // Flex & Alignment 17 | 'flex items-center gap-x-1.5 ', 18 | 19 | // Shape 20 | 'rounded-md', 21 | 22 | // Color 23 | 'text-surface-500 dark:text-white/70', 24 | 25 | // States 26 | 'focus-visible:outline-none focus-visible:outline-offset-0', 27 | 'focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400', 28 | 29 | // Transitions 30 | 'transition-shadow duration-200' 31 | ] 32 | }, 33 | icon: { 34 | class: 'text-surface-500 dark:text-white/70' 35 | }, 36 | separator: { 37 | class: [ 38 | // Flex & Alignment 39 | 'flex items-center shrink-0', 40 | 41 | // Color 42 | 'text-surface-500 dark:text-white/70' 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/overlaypanel/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Shape 5 | 'rounded-lg', 6 | 'shadow-xl', 7 | 'border-0 dark:border', 8 | 9 | // Position 10 | 'absolute left-0 top-0 mt-2', 11 | 'z-40 transform origin-center', 12 | 13 | // Color 14 | 'bg-surface-0 dark:bg-surface-800', 15 | 'text-surface-700 dark:text-surface-0/80', 16 | 'dark:border-surface-700', 17 | 18 | // Before: Triangle 19 | 'before:absolute before:-top-2 before:ml-4 before:z-50', 20 | 'before:w-0 before:h-0 before:shadow-xl', 21 | 'before:border-transparent before:border-solid', 22 | 'before:border-x-[0.5rem] before:border-b-[0.5rem]', 23 | 'before:border-t-0 before:border-b-surface-0 dark:before:border-b-surface-800' 24 | ] 25 | }, 26 | content: { 27 | class: 'p-6 items-center flex' 28 | }, 29 | transition: { 30 | enterFromClass: 'opacity-0 scale-y-[0.8]', 31 | enterActiveClass: 'transition-[transform,opacity] duration-[120ms] ease-[cubic-bezier(0,0,0.2,1)]', 32 | leaveActiveClass: 'transition-opacity duration-100 ease-linear', 33 | leaveToClass: 'opacity-0' 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/terminal/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Spacing 5 | 'p-5', 6 | 7 | // Shape 8 | 'rounded-md', 9 | 10 | // Color 11 | 'bg-surface-900 text-white', 12 | 'border border-surface-700', 13 | 14 | // Sizing & Overflow 15 | 'h-72 overflow-auto' 16 | ] 17 | }, 18 | container: { 19 | class: [ 20 | // Flexbox 21 | 'flex items-center' 22 | ] 23 | }, 24 | prompt: { 25 | class: [ 26 | // Color 27 | 'text-surface-400' 28 | ] 29 | }, 30 | response: { 31 | class: [ 32 | // Color 33 | 'text-primary-400' 34 | ] 35 | }, 36 | command: { 37 | class: [ 38 | // Color 39 | 'text-primary-400' 40 | ] 41 | }, 42 | commandtext: { 43 | class: [ 44 | // Flexbox 45 | 'flex-1 shrink grow-0', 46 | 47 | // Shape 48 | 'border-0', 49 | 50 | // Spacing 51 | 'p-0', 52 | 53 | // Color 54 | 'bg-transparent text-inherit', 55 | 56 | // Outline 57 | 'outline-none' 58 | ] 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | /dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .aider* 7 | AIDER-CONVENTIONS.md 8 | 9 | # Logs 10 | /ui-shell/logs 11 | /ui-shell/*.log 12 | /ui-shell/npm-debug.log* 13 | /ui-shell/yarn-debug.log* 14 | /ui-shell/yarn-error.log* 15 | /ui-shell/pnpm-debug.log* 16 | /ui-shell/lerna-debug.log* 17 | 18 | /ui-shell/node_modules 19 | /ui-shell/.DS_Store 20 | /ui-shell/dist/**.* 21 | 22 | # We use the assets folder so we don't have to build 23 | # because we are yet to publish the UI package on NPM 24 | # https://github.com/damms005/devdb-vscode/actions/runs/8435299388/job/23100385334#step:6:25 25 | # https://github.com/damms005/devdb-vscode/blob/d7220957422d1929aec3e51db18e60a54a2e37c2/src/services/html.ts#L57 26 | !/ui-shell/dist/assets/**.* 27 | 28 | /ui-shell/dist-ssr 29 | /ui-shell/coverage 30 | /ui-shell/*.local 31 | 32 | /ui-shell/cypress/videos/ 33 | /ui-shell/cypress/screenshots/ 34 | 35 | # Editor directories and files 36 | /ui-shell/.vscode/* 37 | /ui-shell/!.vscode/extensions.json 38 | /ui-shell/.idea 39 | /ui-shell/*.suo 40 | /ui-shell/*.ntvs* 41 | /ui-shell/*.njsproj 42 | /ui-shell/*.sln 43 | /ui-shell/*.sw? 44 | 45 | .DS_Store 46 | .env 47 | 48 | # Dynamic Dockerfiles generated by boot scripts 49 | apps/*/Dockerfile 50 | 51 | # Todo app directory contents (except .gitkeep) 52 | apps/rails/todo-app/* 53 | !apps/rails/todo-app/.gitkeep 54 | 55 | ltex* 56 | -------------------------------------------------------------------------------- /src/services/laravel/sail.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { parse } from 'yaml' 3 | import { fileExists, getBasePath, getWorkspaceFileContent } from "../workspace"; 4 | import { getEnvFileValue } from "./laravel-core"; 5 | import { KnexClientType } from "../../types"; 6 | 7 | export async function hasLaravelSailDockerComposeFile() { 8 | const workspacePath = getBasePath() 9 | if (!workspacePath) return false 10 | 11 | const dockerComposeFilePath = join(workspacePath, 'docker-compose.yml'); 12 | 13 | const exists = await fileExists(dockerComposeFilePath) 14 | 15 | return exists 16 | } 17 | 18 | export async function getPortFromDockerCompose(dialect: KnexClientType): Promise { 19 | const dockerComposeContent = (getWorkspaceFileContent('docker-compose.yml'))?.toString() 20 | if (!dockerComposeContent) return 21 | 22 | const dockerComposeParsed = parse(dockerComposeContent) 23 | 24 | const portDefinition: string = dockerComposeParsed.services?.[dialect]?.ports[0].toString() 25 | if (!portDefinition) return 26 | 27 | // Match string like '${FORWARD_DB_PORT:-3307}:3306' where FORWARD_DB_PORT and 3307 are captured 28 | const match = portDefinition.match(/\${(\w+):-(\d+)}:\d+/) 29 | if (!match) return 30 | 31 | const [, envVariable, defaultPort,] = match 32 | const port: string = await getEnvFileValue(envVariable) || defaultPort 33 | 34 | return parseInt(port) 35 | } -------------------------------------------------------------------------------- /ui-shell/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | ### Run Headed Component Tests with [Cypress Component Testing](https://on.cypress.io/component) 32 | 33 | ```sh 34 | npm run test:unit:dev # or `npm run test:unit` for headless testing 35 | ``` 36 | 37 | ### Run End-to-End Tests with [Cypress](https://www.cypress.io/) 38 | 39 | ```sh 40 | npm run test:e2e:dev 41 | ``` 42 | 43 | This runs the end-to-end tests against the Vite development server. 44 | It is much faster than the production build. 45 | 46 | But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): 47 | 48 | ```sh 49 | npm run build 50 | npm run test:e2e 51 | ``` 52 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/avatar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props, parent }) => ({ 3 | class: [ 4 | // Font 5 | { 6 | 'text-sm': props.size == null || props.size == 'normal', 7 | 'text-lg': props.size == 'large', 8 | 'text-xl': props.size == 'xlarge' 9 | }, 10 | 11 | // Alignments 12 | 'inline-flex items-center justify-center', 13 | 'shrink-0', 14 | 'relative', 15 | 16 | // Sizes 17 | { 18 | 'h-8 w-8': props.size == null || props.size == 'normal', 19 | 'w-12 h-12': props.size == 'large', 20 | 'w-16 h-16': props.size == 'xlarge' 21 | }, 22 | { '-ml-4': parent.instance.$style?.name == 'avatargroup' }, 23 | 24 | // Shapes 25 | { 26 | 'rounded-lg': props.shape == 'square', 27 | 'rounded-full': props.shape == 'circle' 28 | }, 29 | { 'border-2': parent.instance.$style?.name == 'avatargroup' }, 30 | 31 | // Colors 32 | 'bg-surface-100 dark:bg-surface-700', 33 | { 'border-white dark:border-surface-800': parent.instance.$style?.name == 'avatargroup' } 34 | ] 35 | }), 36 | image: { 37 | class: 'h-full w-full' 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /ui-shell/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}', './src/services/html.ts'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'primary-50': 'rgb(var(--primary-50))', 8 | 'primary-100': 'rgb(var(--primary-100))', 9 | 'primary-200': 'rgb(var(--primary-200))', 10 | 'primary-300': 'rgb(var(--primary-300))', 11 | 'primary-400': 'rgb(var(--primary-400))', 12 | 'primary-500': 'rgb(var(--primary-500))', 13 | 'primary-600': 'rgb(var(--primary-600))', 14 | 'primary-700': 'rgb(var(--primary-700))', 15 | 'primary-800': 'rgb(var(--primary-800))', 16 | 'primary-900': 'rgb(var(--primary-900))', 17 | 'primary-950': 'rgb(var(--primary-950))', 18 | 'surface-0': 'rgb(var(--surface-0))', 19 | 'surface-50': 'rgb(var(--surface-50))', 20 | 'surface-100': 'rgb(var(--surface-100))', 21 | 'surface-200': 'rgb(var(--surface-200))', 22 | 'surface-300': 'rgb(var(--surface-300))', 23 | 'surface-400': 'rgb(var(--surface-400))', 24 | 'surface-500': 'rgb(var(--surface-500))', 25 | 'surface-600': 'rgb(var(--surface-600))', 26 | 'surface-700': 'rgb(var(--surface-700))', 27 | 'surface-800': 'rgb(var(--surface-800))', 28 | 'surface-900': 'rgb(var(--surface-900))', 29 | 'surface-950': 'rgb(var(--surface-950))', 30 | }, 31 | }, 32 | }, 33 | plugins: [], 34 | } 35 | -------------------------------------------------------------------------------- /apps/django/README.md: -------------------------------------------------------------------------------- 1 | # Django Dynamic Database Boot System 2 | 3 | A script that generates and runs Django applications with different database configurations on-demand. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Run with SQLite (default) 9 | ./boot.sh 10 | 11 | # Run with MySQL 12 | ./boot.sh mysql 13 | 14 | # Run with PostgreSQL 15 | ./boot.sh postgres 16 | ``` 17 | 18 | ## Prerequisites 19 | 20 | - Python 3.7+ 21 | - Docker (for MySQL/PostgreSQL) 22 | 23 | ## Features 24 | 25 | - Supports SQLite, MySQL, and PostgreSQL 26 | - Automatically creates a sample todo application 27 | - Loads demo data for testing 28 | - Finds available ports automatically 29 | - Cleans up when stopped (Ctrl+C) 30 | 31 | ## Database Setup 32 | 33 | ### MySQL 34 | ```bash 35 | docker run -d --name mysql-devdb-triage -p 2222:3306 -e MYSQL_ROOT_PASSWORD=mysecretpassword mysql:8.0 36 | ``` 37 | 38 | ### PostgreSQL 39 | ```bash 40 | docker run -d --name postgres-devdb-triage -p 3333:5432 -e POSTGRES_PASSWORD=mysecretpassword postgres:16 41 | ``` 42 | 43 | ## Note 44 | 45 | - Connection details are printed when the script runs 46 | - The application is ephemeral - all data is lost when stopped 47 | - The generated application is ignored by Git 48 | 49 | ## Troubleshooting 50 | 51 | - For database errors: Check if containers are running 52 | - For port conflicts: The script will find an available port 53 | - If Django is missing: Run `pipx install django` 54 | -------------------------------------------------------------------------------- /src/services/pagination.ts: -------------------------------------------------------------------------------- 1 | export type PaginationData = { 2 | currentPage: number; 3 | firstRowOnPage: number; 4 | lastRowOnPage: number; 5 | totalRows: number; 6 | prevPage?: number; 7 | nextPage?: number; 8 | endPage?: number; 9 | itemsPerPage: number; 10 | displayText: string; 11 | }; 12 | 13 | export function getPaginationFor(table: string, page: number, totalRows: number, itemsPerPage: number): PaginationData { 14 | if (totalRows === 0) { 15 | return { 16 | currentPage: 1, 17 | firstRowOnPage: 0, 18 | lastRowOnPage: 0, 19 | totalRows: totalRows, 20 | endPage: 1, 21 | itemsPerPage, 22 | displayText: `Showing 0 to 0 of 0 records` 23 | }; 24 | } 25 | 26 | const endPage = Math.ceil(totalRows / itemsPerPage); 27 | // Ensure the current page is within valid bounds 28 | const validPage = Math.min(Math.max(page, 1), endPage); 29 | 30 | const firstRowOnPage = (validPage - 1) * itemsPerPage + 1; 31 | const lastRowOnPage = validPage === endPage ? totalRows : validPage * itemsPerPage; 32 | 33 | return { 34 | currentPage: validPage, 35 | firstRowOnPage, 36 | lastRowOnPage, 37 | totalRows: totalRows, 38 | prevPage: validPage > 1 ? validPage - 1 : undefined, 39 | nextPage: validPage < endPage ? validPage + 1 : undefined, 40 | endPage, 41 | itemsPerPage, 42 | displayText: `Showing ${firstRowOnPage.toLocaleString()} to ${lastRowOnPage.toLocaleString()} of ${totalRows.toLocaleString()} records` 43 | }; 44 | } -------------------------------------------------------------------------------- /apps/rails/README.md: -------------------------------------------------------------------------------- 1 | # Rails Dynamic Database Boot System 2 | 3 | A script that generates and runs Rails applications with different database configurations on-demand. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Run with SQLite (default) 9 | ./boot.sh 10 | 11 | # Run with MySQL 12 | ./boot.sh mysql 13 | 14 | # Run with PostgreSQL 15 | ./boot.sh postgres 16 | ``` 17 | 18 | ## Prerequisites 19 | 20 | - Ruby 3.0+ 21 | - Rails 8.0+ 22 | - Docker (for MySQL/PostgreSQL) 23 | 24 | ## Features 25 | 26 | - Supports SQLite, MySQL, and PostgreSQL 27 | - Automatically creates a sample todo application 28 | - Loads demo data for testing 29 | - Finds available ports automatically (starting from 3000) 30 | - Cleans up when stopped (Ctrl+C) 31 | 32 | ## Database Setup 33 | 34 | ### MySQL 35 | ```bash 36 | docker run -d --name mysql-devdb-triage -p 2222:3306 -e MYSQL_ROOT_PASSWORD=mysecretpassword mysql:8.0 37 | ``` 38 | 39 | ### PostgreSQL 40 | ```bash 41 | docker run -d --name postgres-devdb-triage -p 3333:5432 -e POSTGRES_PASSWORD=mysecretpassword postgres:16 42 | ``` 43 | 44 | ## Note 45 | 46 | - Connection details are printed when the script runs 47 | - The application is ephemeral - all data is lost when stopped 48 | - The generated application is ignored by Git 49 | 50 | ## Troubleshooting 51 | 52 | - For database errors: Check if containers are running 53 | - For port conflicts: The script will find an available port 54 | - If Rails is missing: Run `gem install rails` 55 | -------------------------------------------------------------------------------- /src/services/config-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { CosmiconfigResult, cosmiconfig } from 'cosmiconfig'; 3 | import { SqliteConfig, MysqlConfig, MssqlConfig, PostgresConfig } from '../types'; 4 | import { getPathToWorkspaceFile } from './workspace'; 5 | 6 | export const DEVDB_CONFIG_FILE_NAME = '.devdbrc' 7 | 8 | export function getConfigFilePath() { 9 | return getPathToWorkspaceFile(DEVDB_CONFIG_FILE_NAME) 10 | } 11 | 12 | export async function getConfigFileContent(): Promise<(SqliteConfig | MysqlConfig | MssqlConfig | PostgresConfig)[] | undefined> { 13 | const configFilePath = getConfigFilePath() 14 | if (!configFilePath) return 15 | 16 | try { 17 | const result: CosmiconfigResult = await cosmiconfig('devdb').load(configFilePath) 18 | if (!result) return 19 | 20 | return result.config as (SqliteConfig | MysqlConfig)[] 21 | } catch (error) { 22 | } 23 | } 24 | 25 | /** 26 | * Adds a SQLite database to the config file if it doesn't already exist 27 | */ 28 | export async function addSqlDatabaseToConfig(sqliteFilePath: string) { 29 | if (!sqliteFilePath) return 30 | 31 | const config = await getConfigFileContent() || [] 32 | const configExists = config.some(config => config.type === 'sqlite' && config.path === sqliteFilePath) 33 | 34 | if (configExists) return 35 | 36 | config.push({ 37 | type: 'sqlite', 38 | path: sqliteFilePath 39 | }) 40 | 41 | await vscode.workspace.fs.writeFile( 42 | vscode.Uri.file(getConfigFilePath() as string), 43 | Buffer.from(JSON.stringify(config, null, 2)) 44 | ) 45 | } -------------------------------------------------------------------------------- /src/services/connector.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import knexlib from "knex"; 3 | import { log } from './logging-service'; 4 | import { logToOutput } from './output-service'; 5 | import { KnexClientType } from '../types'; 6 | 7 | export async function getConnectionFor(description: string, dialect: KnexClientType, host: string, port: number, username: string, password: string, database: string | undefined = undefined, notifyOnError = true, options?: Record): Promise { 8 | 9 | log(`Connector - ${description}`, `Attempting to connect to database: dialect=${dialect}, host=${host}, port=${port}, username=${username}, database=${database ? String(database[0]) + '*****' : ''}`); 10 | 11 | try { 12 | const connection: any = { 13 | host: host ? String(host) : host, 14 | port: port ? Number(port) : port, 15 | user: username ? String(username) : username, 16 | password: password ? String(password) : password, 17 | database: database ? String(database) : database, 18 | }; 19 | 20 | // Add options to connection config for MSSQL 21 | if (dialect === 'mssql' && options) { 22 | connection.options = options; 23 | } 24 | 25 | const knex = knexlib.knex({ 26 | client: dialect, 27 | connection, 28 | }); 29 | 30 | return knex 31 | } catch (error) { 32 | const message = `Connection error for '${dialect} dialect': ${String(error)}` 33 | if (notifyOnError) { 34 | vscode.window.showErrorMessage(message) 35 | } 36 | 37 | logToOutput(message, 'Connector') 38 | 39 | return; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/services/context-menu-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ModelMap } from '../types'; 3 | import { getTableModelMapForCurrentWorkspace } from './codelens/laravel/laravel-codelens-service'; 4 | import { getWordUnderCursor } from './document-service'; 5 | import { LaravelFactoryGenerator } from './laravel/factory-generator'; 6 | import { database } from './messenger'; 7 | import { showMissingDatabaseNotification } from './error-notification-service'; 8 | import { explainSelectedQuery } from './codelens/laravel/sql-query-explainer-provider'; 9 | 10 | export async function contextMenuLaravelFactoryGenerator() { 11 | if (!database) { 12 | return showMissingDatabaseNotification() 13 | } 14 | 15 | const wordUnderCursor = getWordUnderCursor() 16 | if (!wordUnderCursor) { 17 | return vscode.window.showErrorMessage('No word under cursor'); 18 | } 19 | 20 | const tableModelMap: ModelMap = await getTableModelMapForCurrentWorkspace() 21 | const model = tableModelMap[wordUnderCursor]; 22 | 23 | if (!model) { 24 | return vscode.window.showErrorMessage(`No model found by name ${wordUnderCursor}`); 25 | } 26 | 27 | const generator = new LaravelFactoryGenerator(database); 28 | await generator.generateFactory(wordUnderCursor, model.filePath); 29 | } 30 | 31 | export async function contextMenuQueryExplainer() { 32 | const editor = vscode.window.activeTextEditor; 33 | if (!editor) return; 34 | 35 | const document = editor.document; 36 | if (!document) return; 37 | 38 | const selection = editor.selection; 39 | 40 | explainSelectedQuery(document, selection) 41 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/scrolltop/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Flex & Alignment 5 | 'flex items-center justify-center', 6 | 7 | // Positioning 8 | { 9 | sticky: props.target === 'parent', 10 | fixed: props.target === 'window' 11 | }, 12 | 'bottom-[20px] right-[20px]', 13 | 'ml-auto', 14 | 15 | // Shape & Size 16 | { 17 | 'rounded-md h-8 w-8': props.target === 'parent', 18 | 'h-12 w-12 rounded-full shadow-md': props.target === 'window' 19 | }, 20 | 21 | // Color 22 | 'text-white dark:text-surface-900', 23 | { 24 | 'bg-primary-500 dark:bg-primary-400 hover:bg-primary-600 dark:hover:bg-primary-300': props.target === 'parent', 25 | 'bg-surface-500 dark:bg-surface-400 hover:bg-surface-600 dark:hover:bg-surface-300': props.target === 'window' 26 | }, 27 | 28 | // States 29 | { 30 | 'hover:bg-primary-600 dark:hover:bg-primary-300': props.target === 'parent', 31 | 'hover:bg-surface-600 dark:hover:bg-surface-300': props.target === 'window' 32 | } 33 | ] 34 | }), 35 | transition: { 36 | enterFromClass: 'opacity-0', 37 | enterActiveClass: 'transition-opacity duration-150', 38 | leaveActiveClass: 'transition-opacity duration-150', 39 | leaveToClass: 'opacity-0' 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/services/laravel/artisan-service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { exec } from 'child_process'; 3 | import { promisify } from 'util'; 4 | import { getBasePath } from '../workspace'; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | export class ArtisanService { 9 | constructor(private workspaceRoot: string) { } 10 | 11 | static create(): ArtisanService | undefined { 12 | const workspaceRoot = getBasePath(); 13 | if (!workspaceRoot) { 14 | vscode.window.showErrorMessage('No workspace folder found'); 15 | return undefined; 16 | } 17 | return new ArtisanService(workspaceRoot); 18 | } 19 | 20 | async runCommand(command: string, args: string[] = []): Promise { 21 | const config = vscode.workspace.getConfiguration('Devdb'); 22 | const phpPath = config.get('phpExecutablePath') || 'php'; 23 | 24 | const fullCommand = `${phpPath} artisan ${command} ${args.join(' ')}`; 25 | 26 | const { stdout, stderr } = await execAsync( 27 | fullCommand, 28 | { cwd: this.workspaceRoot } 29 | ).catch(error => { 30 | vscode.window.showErrorMessage( 31 | `Failed to run artisan command: ${error.message}` 32 | ); 33 | return { stdout: '', stderr: error.message }; 34 | }); 35 | 36 | if (stderr) { 37 | vscode.window.showErrorMessage( 38 | `Error running artisan command: ${stderr}` 39 | ); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If the string is longer than 30 chars, it will be shortened to 30 chars with ellipsis. It will be shortened by 3 | * adding the first 14 chars and the last 13 chars of the string together with an ellipsis in the middle. 4 | * e.g. /home/damms005/.SchoolServer/test-workspace/database.db3 becomes /home/damms005.../database.db3 5 | */ 6 | export function brief(str: string, length: number = 60) { 7 | if (str.length <= length) return str 8 | 9 | const halfLength = length / 2 10 | 11 | const firstPart = str.substring(0, halfLength) 12 | const lastPart = str.substring(str.length - halfLength) 13 | 14 | return `${firstPart}...${lastPart}` 15 | } 16 | 17 | /** 18 | * Extracts unique variables from a given text 19 | * @param text The input text containing variables 20 | * @returns An array of unique variables found 21 | */ 22 | export function extractVariables(text: string): string[] { 23 | const variableRegex = /\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:->[\w]+)+/g; 24 | return [...new Set(text.match(variableRegex) || [])]; 25 | } 26 | 27 | /** 28 | * Replaces variables in the text with user-provided values 29 | * @param text The original text with variables 30 | * @param variableValues A map of variables to their replacement values 31 | * @returns The text with variables replaced 32 | */ 33 | export function replaceVariables(text: string, variableValues: { [key: string]: string }): string { 34 | const variableRegex = /\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:->[\w]+)+/g; 35 | 36 | return text.replace(variableRegex, (match) => { 37 | return variableValues[match] || match; 38 | }); 39 | } -------------------------------------------------------------------------------- /src/services/laravel/laravel-core.ts: -------------------------------------------------------------------------------- 1 | import { getWorkspaceFileContent } from "../workspace"; 2 | 3 | /** 4 | * Returns the hostname portion of the APP_URL environment variable. 5 | */ 6 | export async function getHostname(): Promise { 7 | const appUrl = await getEnvFileValue('APP_URL') 8 | if (!appUrl) return 9 | 10 | const appUrlWithoutQuotes = appUrl.replace(/"/g, '') 11 | const appUrlWithoutTrailingSlash = appUrlWithoutQuotes.endsWith('/') 12 | ? appUrlWithoutQuotes.substring(0, appUrlWithoutQuotes.length - 1) 13 | : appUrlWithoutQuotes 14 | 15 | const appUrlWithoutProtocol = appUrlWithoutTrailingSlash.replace(/https?:\/\//, '') 16 | const appUrlWithoutPort = appUrlWithoutProtocol.replace(/:\d+/, '') 17 | 18 | return appUrlWithoutPort 19 | } 20 | 21 | export async function getEnvFileValue(envFileKey: string): Promise { 22 | const envFileContents = getWorkspaceFileContent('.env')?.toString() 23 | if (!envFileContents) return 24 | 25 | const lines = envFileContents.split('\n'); 26 | const appUrlLine = lines.find((line: string) => line.startsWith(`${envFileKey}=`)) 27 | if (!appUrlLine) return 28 | 29 | const appUrl = appUrlLine.substring(appUrlLine.indexOf('=') + 1) 30 | const appUrlWithoutQuotes = appUrl.replace(/"/g, '') 31 | const appUrlWithoutTrailingSlash = appUrlWithoutQuotes.endsWith('/') 32 | ? appUrlWithoutQuotes.substring(0, appUrlWithoutQuotes.length - 1) 33 | : appUrlWithoutQuotes 34 | 35 | const appUrlWithoutProtocol = appUrlWithoutTrailingSlash.replace(/https?:\/\//, '') 36 | const appUrlWithoutPort = appUrlWithoutProtocol.replace(/:\d+/, '') 37 | 38 | return appUrlWithoutPort.trim() 39 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inputtext/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props, context, parent }) => ({ 3 | class: [ 4 | // Font 5 | 'font-sans leading-6', 6 | 7 | // Spacing 8 | 'm-0', 9 | { 10 | 'py-3 px-4 text-lg sm:text-md': props.size == 'large', 11 | 'py-1 px-2 sm:text-sm': props.size == 'small', 12 | 'py-1.5 px-3 sm:text-sm': props.size == null 13 | }, 14 | 15 | // Colors 16 | 'text-surface-900 dark:text-surface-0', 17 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500', 18 | 'bg-surface-0 dark:bg-surface-900', 19 | 'shadow-sm', 20 | { 'ring-1 ring-inset ring-surface-300 dark:ring-surface-700 ring-offset-0': parent.instance.$name !== 'InputGroup' }, 21 | 22 | // Shape 23 | { 'rounded-md': parent.instance.$name !== 'InputGroup' }, 24 | { 'first:rounded-l-md rounded-none last:rounded-r-md': parent.instance.$name == 'InputGroup' }, 25 | { 'border-0 border-y border-l last:border-r border-surface-300 dark:border-surface-600': parent.instance.$name == 'InputGroup' }, 26 | { 'first:ml-0 ml-[-1px]': parent.instance.$name == 'InputGroup' && !props.showButtons }, 27 | 'appearance-none', 28 | 29 | // Interactions 30 | { 31 | 'outline-none focus:ring-primary-500 dark:focus:ring-primary-400': !context.disabled, 32 | 'opacity-60 select-none pointer-events-none cursor-default': context.disabled 33 | } 34 | ] 35 | }) 36 | }; 37 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/selectbutton/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: ['shadow-sm', { 'opacity-60 select-none pointer-events-none cursor-default': props.disabled }] 4 | }), 5 | button: ({ context }) => ({ 6 | class: [ 7 | 'relative', 8 | // Font 9 | 'text-sm', 10 | 'leading-none', 11 | 12 | // Flex Alignment 13 | 'inline-flex items-center align-bottom text-center', 14 | 15 | // Spacing 16 | 'px-2.5 py-1.5', 17 | 18 | // Shape 19 | 'ring-1 ring-surface-200 dark:ring-surface-700', 20 | 'first:rounded-l-md first:rounded-tr-none first:rounded-br-none', 21 | 'last:rounded-tl-none last:rounded-bl-none last:rounded-r-md ', 22 | 23 | // Color 24 | { 25 | 'bg-surface-0 dark:bg-surface-900': !context.active, 26 | 'text-surface-700 dark:text-white/80': !context.active, 27 | 'bg-surface-100 dark:bg-surface-700': context.active 28 | }, 29 | 30 | // States 31 | 'focus:outline-none focus:outline-offset-0 focus:ring-primary-500 dark:focus:ring-primary-400 focus:z-10', 32 | 'hover:bg-surface-200 dark:hover:bg-surface-600/80', 33 | { 'opacity-60 select-none pointer-events-none cursor-default': context.disabled }, 34 | 35 | // Transition 36 | 'transition duration-200', 37 | 38 | // Misc 39 | 'cursor-pointer select-none overflow-hidden' 40 | ] 41 | }), 42 | label: { 43 | class: 'font-semibold' 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/providers/postgres/laravel-postgres-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 3 | import { PostgresEngine } from '../../database-engines/postgres-engine'; 4 | import { getConnectionInEnvFile } from '../../services/laravel/env-file-parser'; 5 | import { isDdevProject, isComposerPhpProject } from '../../services/workspace'; 6 | 7 | export const LaravelPostgresProvider: DatabaseEngineProvider = { 8 | name: 'Laravel PostgreSQL', 9 | type: 'postgres', 10 | id: 'laravel-postgres', 11 | description: 'Laravel PostgreSQL with default .env config', 12 | engine: undefined, 13 | 14 | async canBeUsedInCurrentWorkspace(): Promise { 15 | if (!isComposerPhpProject()) { 16 | return false; 17 | } 18 | 19 | if (isDdevProject()) { 20 | /** 21 | * This is simply to improve the DX. Else, we report false negative as 22 | * it tries to load from .env, which is not how DDEV projects work. 23 | * 24 | * @see https://discord.com/channels/664580571770388500/1348955044334141460/1349010258214781021 25 | */ 26 | return false; 27 | } 28 | 29 | const connection = await getConnectionInEnvFile('pgsql', 'postgres') 30 | if (!connection) return false 31 | 32 | try { 33 | this.engine = new PostgresEngine(connection); 34 | } catch (error) { 35 | vscode.window.showErrorMessage(`Postgres connection error: ${String(error)}`) 36 | return false 37 | } 38 | 39 | return (await this.engine.isOkay()) 40 | }, 41 | 42 | reconnect(): Promise { 43 | return this.canBeUsedInCurrentWorkspace() 44 | }, 45 | 46 | async getDatabaseEngine(): Promise { 47 | return this.engine 48 | } 49 | } -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | 3 | const production = process.argv.includes('--production'); 4 | const watch = process.argv.includes('--watch'); 5 | 6 | async function main () { 7 | const ctx = await esbuild.context({ 8 | entryPoints: ['src/extension.ts', 'src/services/mcp/no-vscode/server.ts'], 9 | bundle: true, 10 | format: 'cjs', 11 | minify: production, 12 | sourcemap: !production, 13 | sourcesContent: false, 14 | platform: 'node', 15 | outdir: 'dist', 16 | entryNames: '[dir]/[name]', 17 | 18 | /** 19 | * Reasons for externalizing: 20 | * - vscode: not a typical npm package - injected by the IDE at runtime 21 | * - @vscode/sqlite3: has native bindings 22 | */ 23 | external: ['vscode', '@vscode/sqlite3'], 24 | 25 | metafile: true, 26 | logLevel: 'warning', 27 | plugins: [ 28 | /* add to the end of plugins array */ 29 | esbuildProblemMatcherPlugin 30 | ] 31 | }); 32 | if (watch) { 33 | await ctx.watch(); 34 | } else { 35 | await ctx.rebuild(); 36 | await ctx.dispose(); 37 | } 38 | } 39 | 40 | /** 41 | * @type {import('esbuild').Plugin} 42 | */ 43 | const esbuildProblemMatcherPlugin = { 44 | name: 'esbuild-problem-matcher', 45 | 46 | setup (build) { 47 | build.onStart(() => { 48 | console.log('[watch] build started'); 49 | }); 50 | build.onEnd(result => { 51 | result.errors.forEach(({ text, location }) => { 52 | console.error(`✘ [ERROR] ${text}`); 53 | if (location == null) return; 54 | console.error(` ${location.file}:${location.line}:${location.column}:`); 55 | }); 56 | 57 | console.log('[watch] build finished'); 58 | }); 59 | } 60 | }; 61 | 62 | main().catch(e => { 63 | console.error(e); 64 | process.exit(1); 65 | }); 66 | -------------------------------------------------------------------------------- /src/uri-handler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class DevDbUriHandler implements vscode.UriHandler { 4 | public handleUri(uri: vscode.Uri): vscode.ProviderResult { 5 | if (!uri.path.startsWith('/open/table')) { 6 | return; 7 | } 8 | 9 | const queryParams = new URLSearchParams(uri.query); 10 | const connectionId = queryParams.get('connectionId'); 11 | const database = queryParams.get('database'); 12 | const table = queryParams.get('table'); 13 | const workspace = queryParams.get('workspace'); 14 | const authority = queryParams.get('authority'); 15 | 16 | if (!table) { 17 | vscode.window.showErrorMessage('DevDb: Missing required parameter "table" in URI'); 18 | return; 19 | } 20 | 21 | if (workspace) { 22 | const currentWorkspaceFolders = vscode.workspace.workspaceFolders; 23 | if (!currentWorkspaceFolders || !currentWorkspaceFolders.some(folder => 24 | folder.uri.fsPath === workspace || 25 | folder.uri.path === workspace)) { 26 | return; 27 | } 28 | } 29 | 30 | if (authority) { 31 | const currentAuthority = vscode.env.remoteName; 32 | if (currentAuthority !== authority) { 33 | return; 34 | } 35 | } 36 | 37 | const params: any = { table: table }; 38 | 39 | if (connectionId) { 40 | params.connectionId = connectionId; 41 | } 42 | 43 | if (database) { 44 | params.database = database; 45 | } 46 | 47 | return vscode.commands.executeCommand('devdb.openTable', params); 48 | } 49 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/badge/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props, context }) => ({ 3 | class: [ 4 | // Font 5 | 'font-medium', 6 | { 7 | 'text-xs leading-[1.5rem]': props.size == null, 8 | 'text-lg leading-[2.25rem]': props.size == 'large', 9 | 'text-2xl leading-[3rem]': props.size == 'xlarge' 10 | }, 11 | 12 | // Alignment 13 | 'text-center inline-block', 14 | 15 | // Size 16 | 'p-0 px-1', 17 | { 18 | 'min-w-[1.5rem] h-[1.5rem]': props.size == null, 19 | 'min-w-[2.25rem] h-[2.25rem]': props.size == 'large', 20 | 'min-w-[3rem] h-[3rem]': props.size == 'xlarge' 21 | }, 22 | 23 | // Shape 24 | { 25 | 'rounded-full': props.value.length == 1, 26 | 'rounded-[0.71rem]': props.value.length !== 1 27 | }, 28 | 29 | // Color 30 | 'text-white dark:text-surface-900', 31 | { 32 | 'bg-primary-500 dark:bg-primary-400': props.severity == null || props.severity == 'primary', 33 | 'bg-surface-500 dark:bg-surface-400': props.severity == 'secondary', 34 | 'bg-green-500 dark:bg-green-400': props.severity == 'success', 35 | 'bg-blue-500 dark:bg-blue-400': props.severity == 'info', 36 | 'bg-orange-500 dark:bg-orange-400': props.severity == 'warning', 37 | 'bg-purple-500 dark:bg-purple-400': props.severity == 'help', 38 | 'bg-red-500 dark:bg-red-400': props.severity == 'danger' 39 | } 40 | ] 41 | }) 42 | }; 43 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/badgedirective/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ context }) => ({ 3 | class: [ 4 | // Font 5 | 'font-medium', 6 | 'text-xs leading-6 font-sans', 7 | 8 | // Alignment 9 | 'flex items-center justify-center', 10 | 'text-center', 11 | 12 | // Position 13 | 'absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 origin-top-right', 14 | 15 | // Size 16 | 'm-0', 17 | { 18 | 'p-0': context.nogutter || context.dot, 19 | 'p-1': !context.nogutter && !context.dot, 20 | 'min-w-[0.5rem] h-2': context.dot, 21 | 'min-w-[1rem] h-4': !context.dot 22 | }, 23 | 24 | // Shape 25 | { 26 | 'rounded-full': context.nogutter || context.dot, 27 | 'rounded-[10px]': !context.nogutter && !context.dot 28 | }, 29 | 30 | // Color 31 | 'text-white dark:text-surface-900', 32 | 'ring-1 ring-white dark:ring-surface-900', 33 | { 34 | 'bg-primary-500 dark:bg-primary-400': !context.info && !context.success && !context.warning && !context.danger && !context.help && !context.secondary, 35 | 'bg-surface-500 dark:bg-surface-400': context.secondary, 36 | 'bg-green-500 dark:bg-green-400': context.success, 37 | 'bg-blue-500 dark:bg-blue-400': context.info, 38 | 'bg-orange-500 dark:bg-orange-400': context.warning, 39 | 'bg-purple-500 dark:bg-purple-400': context.help, 40 | 'bg-red-500 dark:bg-red-400': context.danger 41 | } 42 | ] 43 | }) 44 | }; 45 | -------------------------------------------------------------------------------- /src/providers/sqlite/file-picker-sqlite-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from "../../types"; 3 | import { addSqlDatabaseToConfig } from '../../services/config-service'; 4 | import { SqliteEngine } from '../../database-engines/sqlite-engine'; 5 | 6 | export const FilePickerSqliteProvider: DatabaseEngineProvider = { 7 | name: 'SQLite Database File Picker', 8 | type: 'sqlite', 9 | id: 'file-picker-sqlite', 10 | description: 'SQLite database file from your computer', 11 | engine: undefined, 12 | isDefault: true, 13 | 14 | async canBeUsedInCurrentWorkspace(): Promise { 15 | return true; 16 | }, 17 | 18 | reconnect(): Promise { 19 | return this.canBeUsedInCurrentWorkspace() 20 | }, 21 | 22 | async getDatabaseEngine(): Promise { 23 | const filePath = await selectFile(); 24 | if (!filePath) { 25 | vscode.window.showErrorMessage('No file selected.') 26 | return 27 | } 28 | 29 | this.engine = new SqliteEngine(filePath); 30 | 31 | let isOkay = false; 32 | try { 33 | isOkay = (await this.engine.isOkay()) 34 | } catch (error) { 35 | vscode.window.showErrorMessage(`Error opening ${filePath}: ${String(error)}`) 36 | return 37 | } 38 | 39 | if (!isOkay) { 40 | vscode.window.showErrorMessage('The selected file is not a valid SQLite database.') 41 | return 42 | } 43 | 44 | await addSqlDatabaseToConfig(filePath) 45 | 46 | return this.engine 47 | } 48 | } 49 | 50 | async function selectFile(): Promise { 51 | const fileUri = await vscode.window.showOpenDialog({ 52 | canSelectMany: false, 53 | openLabel: 'Open SQLite File', 54 | canSelectFolders: false, 55 | title: 'Select SQLite File', 56 | filters: { 'SQLite': ['sqlite', 'db'], 'All Files': ['*'] } 57 | }) 58 | 59 | if (fileUri && fileUri[0]) { 60 | return fileUri[0].fsPath; 61 | } 62 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/scrollpanel/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | wrapper: { 3 | class: [ 4 | // Size & Position 5 | 'h-full w-full', 6 | 7 | // Layering 8 | 'z-[1]', 9 | 10 | // Spacing 11 | 'overflow-hidden', 12 | 13 | // Misc 14 | 'relative float-left' 15 | ] 16 | }, 17 | content: { 18 | class: [ 19 | // Size & Spacing 20 | 'h-[calc(100%+12px)] w-[calc(100%+12px)] pr-[12px] pb-[12px] pl-0 pt-0', 21 | 22 | // Overflow & Scrollbar 23 | 'overflow-scroll scrollbar-none', 24 | 25 | // Box Model 26 | 'box-border', 27 | 28 | // Position 29 | 'relative', 30 | 31 | // Webkit Specific 32 | '[&::-webkit-scrollbar]:hidden' 33 | ] 34 | }, 35 | barX: { 36 | class: [ 37 | // Size & Position 38 | 'h-[6px] bottom-0', 39 | 40 | // Appearance 41 | 'bg-surface-100 dark:bg-surface-700 rounded', 42 | 43 | // Interactivity 44 | 'cursor-pointer', 45 | 46 | // Visibility & Layering 47 | 'invisible z-20', 48 | 49 | // Transition 50 | 'transition duration-[250ms] ease-linear', 51 | 52 | // Misc 53 | 'relative' 54 | ] 55 | }, 56 | barY: { 57 | class: [ 58 | // Size & Position 59 | 'w-[6px] top-0', 60 | 61 | // Appearance 62 | 'bg-surface-100 dark:bg-surface-700 rounded', 63 | 64 | // Interactivity 65 | 'cursor-pointer', 66 | 67 | // Visibility & Layering 68 | 'z-20', 69 | 70 | // Transition 71 | 'transition duration-[250ms] ease-linear', 72 | 73 | // Misc 74 | 'relative' 75 | ] 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/providers/mysql/ddev-mysql-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { MysqlEngine } from '../../database-engines/mysql-engine'; 3 | import { isDdevAvailable, getDatabaseConnection } from '../../services/ddev/ddev-service'; 4 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 5 | import { isDdevProject } from '../../services/workspace'; 6 | import { logToOutput } from '../../services/output-service'; 7 | 8 | export const DdevMysqlProvider: DatabaseEngineProvider = { 9 | name: 'DDEV - MySQL', 10 | type: 'mysql', 11 | id: 'ddev-mysql', 12 | ddev: true, 13 | description: 'MySQL databases in projects running in DDEV', 14 | engine: undefined, 15 | 16 | /** 17 | * Checks if this provider can be used in the current workspace 18 | * @returns Promise true if DDEV is active and MySQL connection is available 19 | */ 20 | async canBeUsedInCurrentWorkspace(): Promise { 21 | try { 22 | if (!isDdevProject()) { 23 | logToOutput('Not a DDEV project', 'Postgres DDEV') 24 | return false; 25 | } 26 | 27 | // Check if DDEV is available 28 | const isDdevActive = await isDdevAvailable(this.name); 29 | 30 | if (!isDdevActive) { 31 | return false; 32 | } 33 | 34 | // Get database connection from DDEV 35 | const connection = await getDatabaseConnection('mysql2'); 36 | 37 | if (!connection) { 38 | return false; 39 | } 40 | 41 | // Initialize the engine 42 | this.engine = new MysqlEngine(connection); 43 | 44 | // Verify the connection is working 45 | return await this.engine.isOkay(); 46 | } catch (error) { 47 | vscode.window.showErrorMessage(`Error initializing MySQL DDEV provider: ${error}`); 48 | return false; 49 | } 50 | }, 51 | 52 | reconnect(): Promise { 53 | return this.canBeUsedInCurrentWorkspace() 54 | }, 55 | 56 | async getDatabaseEngine(): Promise { 57 | return this.engine 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/services/workspace.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { existsSync, readFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | import * as vscode from 'vscode'; 6 | 7 | export function getBasePath(): string | undefined { 8 | 9 | const customBasePath = vscode.workspace.getConfiguration('Devdb').get('customBasePath'); 10 | 11 | if (customBasePath && customBasePath.trim() !== '' && fs.existsSync(customBasePath)) { 12 | return customBasePath; 13 | } 14 | 15 | const workspaceFolders = vscode.workspace.workspaceFolders; 16 | 17 | if (!workspaceFolders || !workspaceFolders.length) return undefined 18 | 19 | return workspaceFolders[0].uri.fsPath; 20 | } 21 | 22 | /** 23 | * Returns the path to the workspace file. 24 | */ 25 | export function getPathToWorkspaceFile(...subPath: string[]): string | undefined { 26 | const firstWorkspacePath = getBasePath() 27 | if (!firstWorkspacePath) return undefined 28 | 29 | return join(firstWorkspacePath, ...subPath); 30 | } 31 | 32 | export function getWorkspaceFileContent(...subPath: string[]): Buffer | undefined { 33 | const filePath = getPathToWorkspaceFile(...subPath) 34 | if (!filePath) return undefined 35 | 36 | if (!existsSync(filePath)) return undefined 37 | 38 | return readFileSync(filePath); 39 | } 40 | 41 | export async function fileExists(path: string): Promise { 42 | try { 43 | await vscode.workspace.fs.stat(vscode.Uri.file(path)) 44 | return true 45 | } catch (error) { 46 | return false 47 | } 48 | } 49 | 50 | export function isDdevProject(): boolean { 51 | const workspaceRoot = getBasePath(); 52 | if (!workspaceRoot) { 53 | return false; 54 | } 55 | 56 | return fs.existsSync(path.join(workspaceRoot, '.ddev')); 57 | } 58 | 59 | export function isComposerPhpProject(): boolean { 60 | // simply check if workspace root contains a .ddev directory 61 | const workspaceRoot = getBasePath(); 62 | if (!workspaceRoot) { 63 | return false; 64 | } 65 | 66 | return fs.existsSync(path.join(workspaceRoot, 'composer.json')); 67 | } -------------------------------------------------------------------------------- /src/providers/postgres/ddev-postgres-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { PostgresEngine } from '../../database-engines/postgres-engine'; 3 | import { isDdevAvailable, getDatabaseConnection } from '../../services/ddev/ddev-service'; 4 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 5 | import { isDdevProject } from '../../services/workspace'; 6 | import { logToOutput } from '../../services/output-service'; 7 | 8 | export const DdevPostgresProvider: DatabaseEngineProvider = { 9 | name: 'DDEV - PostgreSQL', 10 | type: 'postgres', 11 | id: 'ddev-postgres', 12 | ddev: true, 13 | description: 'PostgreSQL databases in projects running in DDEV', 14 | engine: undefined as PostgresEngine | undefined, 15 | 16 | /** 17 | * Checks if this provider can be used in the current workspace 18 | * @returns Promise indicating if the provider can be used 19 | */ 20 | async canBeUsedInCurrentWorkspace(): Promise { 21 | try { 22 | if (!isDdevProject()) { 23 | logToOutput('Not a DDEV project', 'Postgres DDEV') 24 | return false; 25 | } 26 | 27 | // Check if DDEV is available 28 | if (!(await isDdevAvailable(this.name))) { 29 | return false; 30 | } 31 | 32 | // Get PostgreSQL connection from DDEV 33 | const connection = await getDatabaseConnection('postgres'); 34 | if (!connection) { 35 | return false; 36 | } 37 | 38 | // Initialize the engine with the connection 39 | this.engine = new PostgresEngine(connection); 40 | 41 | // Check if the engine is okay 42 | return await this.engine.isOkay(); 43 | } catch (error) { 44 | vscode.window.showErrorMessage(`Failed to initialize PostgreSQL engine: ${error instanceof Error ? error.message : String(error)}`); 45 | return false; 46 | } 47 | }, 48 | 49 | reconnect(): Promise { 50 | return this.canBeUsedInCurrentWorkspace() 51 | }, 52 | 53 | async getDatabaseEngine(): Promise { 54 | return this.engine 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/providers/mysql/laravel-mysql-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 3 | import { MysqlEngine } from '../../database-engines/mysql-engine'; 4 | import { getConnectionInEnvFile } from '../../services/laravel/env-file-parser'; 5 | import { log } from '../../services/logging-service'; 6 | import { isComposerPhpProject, isDdevProject } from '../../services/workspace'; 7 | 8 | export const LaravelMysqlProvider: DatabaseEngineProvider = { 9 | name: 'Laravel Mysql (with Sail support)', 10 | type: 'mysql', 11 | id: 'laravel-mysql', 12 | description: 'Laravel MySQL with default .env config or Sail config in docker-compose.yml', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isComposerPhpProject()) { 17 | return false; 18 | } 19 | 20 | if (isDdevProject()) { 21 | /** 22 | * This is simply to improve the DX. Else, we report false negative as 23 | * it tries to load from .env, which is not how DDEV projects work. 24 | * 25 | * @see https://discord.com/channels/664580571770388500/1348955044334141460/1349010258214781021 26 | */ 27 | return false; 28 | } 29 | 30 | log('Laravel MySQL', 'Checking if Laravel MySQL provider can be used in the current workspace...'); 31 | const connection = await getConnectionInEnvFile('mysql', 'mysql2'); 32 | log('Laravel MySQL', `Connection status: ${connection ? 'successful' : 'failed'}`); 33 | if (!connection) return false 34 | 35 | try { 36 | log('Laravel MySQL', 'Creating MySQL engine...'); 37 | this.engine = new MysqlEngine(connection); 38 | } catch (error) { 39 | vscode.window.showErrorMessage(`MySQL connection error: ${String(error)}`); 40 | log('Laravel MySQL', `MySQL connection error: ${String(error)}`); 41 | return false 42 | } 43 | 44 | log('Laravel MySQL', 'OK'); 45 | return (await this.engine.isOkay()) 46 | }, 47 | 48 | reconnect(): Promise { 49 | return this.canBeUsedInCurrentWorkspace() 50 | }, 51 | 52 | async getDatabaseEngine(): Promise { 53 | return this.engine 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/dataviewlayoutoptions/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | listbutton: ({ props }) => ({ 3 | class: [ 4 | // Font 5 | 'leading-none', 6 | 7 | // Flex Alignment 8 | 'inline-flex items-center align-bottom text-center', 9 | 10 | // Shape 11 | 'rounded-md rounded-r-none', 12 | 13 | // Spacing 14 | 'px-2.5 py-1.5', 15 | 16 | // Color 17 | 'ring-1 ring-surface-200 dark:ring-surface-700', 18 | props.modelValue === 'list' ? 'bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-0' : 'bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-white/80', 19 | 20 | // States 21 | 'focus:outline-none focus:outline-offset-0 focus:ring-primary-500 dark:focus:ring-primary-400', 22 | 'hover:bg-surface-200 dark:hover:bg-surface-600/80', 23 | 24 | // Transition 25 | 'transition duration-200', 26 | 27 | // Misc 28 | 'cursor-pointer select-none overflow-hidden' 29 | ] 30 | }), 31 | gridbutton: ({ props }) => ({ 32 | class: [ 33 | // Font 34 | 'leading-none', 35 | 36 | // Flex Alignment 37 | 'inline-flex items-center align-bottom text-center', 38 | 39 | // Shape 40 | 'rounded-md rounded-l-none', 41 | 42 | // Spacing 43 | 'px-2.5 py-1.5', 44 | 45 | // Color 46 | 'ring-1 ring-surface-200 dark:ring-surface-700', 47 | props.modelValue === 'grid' ? 'bg-surface-100 dark:bg-surface-700 text-surface-700 dark:text-surface-0' : 'bg-surface-0 dark:bg-surface-900 text-surface-700 dark:text-white/80', 48 | 49 | // States 50 | 'focus:outline-none focus:outline-offset-0 focus:ring-primary-500 dark:focus:ring-primary-400', 51 | 'hover:bg-surface-200 dark:hover:bg-surface-600/80', 52 | 53 | // Transition 54 | 'transition duration-200', 55 | 56 | // Misc 57 | 'cursor-pointer select-none overflow-hidden' 58 | ] 59 | }) 60 | }; 61 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/global.js: -------------------------------------------------------------------------------- 1 | export default { 2 | css: ` 3 | *[data-pd-ripple="true"]{ 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | span[data-p-ink-active="true"]{ 8 | animation: ripple 0.4s linear; 9 | } 10 | @keyframes ripple { 11 | 100% { 12 | opacity: 0; 13 | transform: scale(2.5); 14 | } 15 | } 16 | 17 | .progress-spinner-circle { 18 | stroke-dasharray: 89, 200; 19 | stroke-dashoffset: 0; 20 | animation: p-progress-spinner-dash 1.5s ease-in-out infinite, p-progress-spinner-color 6s ease-in-out infinite; 21 | stroke-linecap: round; 22 | } 23 | 24 | @keyframes p-progress-spinner-dash{ 25 | 0% { 26 | stroke-dasharray: 1, 200; 27 | stroke-dashoffset: 0; 28 | } 29 | 30 | 50% { 31 | stroke-dasharray: 89, 200; 32 | stroke-dashoffset: -35px; 33 | } 34 | 100% { 35 | stroke-dasharray: 89, 200; 36 | stroke-dashoffset: -124px; 37 | } 38 | } 39 | @keyframes p-progress-spinner-color { 40 | 100%, 0% { 41 | stroke: #ff5757; 42 | } 43 | 40% { 44 | stroke: #696cff; 45 | } 46 | 66% { 47 | stroke: #1ea97c; 48 | } 49 | 80%, 90% { 50 | stroke: #cc8925; 51 | } 52 | } 53 | 54 | .progressbar-value-animate::after { 55 | will-change: left, right; 56 | animation: p-progressbar-indeterminate-anim-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite; 57 | } 58 | .progressbar-value-animate::before { 59 | will-change: left, right; 60 | animation: p-progressbar-indeterminate-anim 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; 61 | } 62 | @keyframes p-progressbar-indeterminate-anim { 63 | 0% { 64 | left: -35%; 65 | right: 100%; 66 | } 67 | 60% { 68 | left: 100%; 69 | right: -90%; 70 | } 71 | 100% { 72 | left: 100%; 73 | right: -90%; 74 | } 75 | } 76 | ` 77 | }; 78 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/progressbar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Position and Overflow 5 | 'relative overflow-hidden', 6 | 7 | // Shape and Size 8 | 'border-0', 9 | 'rounded-md', 10 | { 'h-7 pt-5': props.mode !== 'indeterminate' && props.showValue }, 11 | { 'h-2 bg-surface-100 dark:bg-surface-700 ': props.mode == 'indeterminate' || !props.showValue }, 12 | 13 | // Before & After (!indeterminate) 14 | { 'before:absolute before:w-full before:rounded-md before:h-2 before:top-[1.25rem] before:left-0 before:bottom-0 before:bg-surface-100 dark:before:bg-surface-700': props.mode !== 'indeterminate' } 15 | ] 16 | }), 17 | value: ({ props }) => ({ 18 | class: [ 19 | // Flexbox & Overflow & Position 20 | { 'absolute flex items-center justify-center': props.mode !== 'indeterminate' }, 21 | 22 | // Colors 23 | 'bg-primary-500 dark:bg-primary-400', 24 | 25 | // Spacing & Sizing 26 | 'm-0', 27 | { 'h-2 w-0': props.mode !== 'indeterminate' }, 28 | 29 | // Shape 30 | 'border-0 rounded-md', 31 | 32 | // Transitions 33 | { 34 | 'transition-width duration-1000 ease-in-out': props.mode !== 'indeterminate', 35 | 'progressbar-value-animate': props.mode == 'indeterminate' 36 | }, 37 | 38 | // Before & After (indeterminate) 39 | { 40 | 'before:absolute before:top-0 before:left-0 before:bottom-0 before:bg-inherit ': props.mode == 'indeterminate', 41 | 'after:absolute after:top-0 after:left-0 after:bottom-0 after:bg-inherit after:delay-1000': props.mode == 'indeterminate' 42 | } 43 | ] 44 | }), 45 | label: { 46 | class: [ 47 | // Flexbox 48 | 'inline-flex justify-end', 49 | 'absolute inset-0 mr-1 -top-[1.15rem]', 50 | 51 | // Font and Text 52 | 'text-sm text-surface-600 dark:text-surface-0/60', 53 | 'leading-none' 54 | ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/rating/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | 'relative', 5 | 6 | // Flex & Alignment 7 | 'flex items-center', 8 | 'gap-1', 9 | 10 | // Misc 11 | { 12 | 'opacity-60 select-none pointer-events-none cursor-default': props.disabled 13 | } 14 | ] 15 | }), 16 | cancelitem: ({ context }) => ({ 17 | class: [ 18 | // Flex & Alignment 19 | 'inline-flex items-center', 20 | 21 | //State 22 | { 23 | 'outline-none ring-2 ring-primary-500 dark:ring-primary-400': context.focused 24 | }, 25 | 26 | // Misc 27 | 'cursor-pointer' 28 | ] 29 | }), 30 | cancelicon: { 31 | class: [ 32 | // Size 33 | 'w-5 h-5', 34 | 35 | // Color 36 | 'text-red-500 dark:text-red-400' 37 | ] 38 | }, 39 | item: ({ props, context }) => ({ 40 | class: [ 41 | // Flex & Alignment 42 | 'inline-flex items-center', 43 | 44 | // State 45 | { 46 | 'outline-none ring-2 ring-primary-500 dark:ring-primary-400': context.focused 47 | }, 48 | 49 | // Misc 50 | { 51 | 'cursor-pointer': !props.readonly, 52 | 'cursor-default': props.readonly 53 | } 54 | ] 55 | }), 56 | officon: ({ props }) => ({ 57 | class: [ 58 | // Size 59 | 'w-5 h-5', 60 | 61 | // Color 62 | 'text-surface-700 dark:text-surface-0/70', 63 | 64 | // State 65 | { 'hover:text-primary-500 dark:hover:text-primary-400': !props.readonly }, 66 | 67 | // Transition 68 | 'transition duration-200 ease-in' 69 | ] 70 | }), 71 | onicon: ({ props }) => ({ 72 | class: [ 73 | // Size 74 | 'w-5 h-5', 75 | 76 | // Color 77 | 'text-primary-500 dark:text-primary-400', 78 | 79 | // Transition 80 | 'transition duration-200 ease-in' 81 | ] 82 | }) 83 | }; 84 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/password/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | 'inline-flex relative', 5 | { 6 | 'opacity-60 select-none pointer-events-none cursor-default': props.disabled 7 | } 8 | ] 9 | }), 10 | panel: { 11 | class: [ 12 | // Spacing 13 | 'p-3', 14 | 15 | // Shape 16 | 'border-0 dark:border', 17 | 'shadow-md rounded-md', 18 | 19 | // Colors 20 | 'bg-surface-0 dark:bg-surface-900', 21 | 'text-surface-700 dark:text-white/80', 22 | 'dark:border-surface-700' 23 | ] 24 | }, 25 | meter: { 26 | class: [ 27 | // Position and Overflow 28 | 'overflow-hidden', 29 | 'relative', 30 | 31 | // Shape and Size 32 | 'border-0', 33 | 'h-2', 34 | 'rounded-md', 35 | 36 | // Spacing 37 | 'mb-2', 38 | 39 | // Colors 40 | 'bg-surface-100 dark:bg-surface-700' 41 | ] 42 | }, 43 | meterlabel: ({ instance }) => ({ 44 | class: [ 45 | // Size 46 | 'h-full', 47 | 48 | // Colors 49 | { 50 | 'bg-red-500 dark:bg-red-400/50': instance?.meter?.strength == 'weak', 51 | 'bg-orange-500 dark:bg-orange-400/50': instance?.meter?.strength == 'medium', 52 | 'bg-green-500 dark:bg-green-400/50': instance?.meter?.strength == 'strong' 53 | }, 54 | 55 | // Transitions 56 | 'transition-all duration-1000 ease-in-out' 57 | ] 58 | }), 59 | showicon: { 60 | class: ['absolute top-1/2 right-3 -mt-2', 'text-surface-600 dark:text-white/70'] 61 | }, 62 | hideicon: { 63 | class: ['absolute top-1/2 right-3 -mt-2', 'text-surface-600 dark:text-white/70'] 64 | }, 65 | transition: { 66 | enterFromClass: 'opacity-0 scale-y-[0.8]', 67 | enterActiveClass: 'transition-[transform,opacity] duration-[120ms] ease-[cubic-bezier(0,0,0.2,1)]', 68 | leaveActiveClass: 'transition-opacity duration-100 ease-linear', 69 | leaveToClass: 'opacity-0' 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/accordion/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | accordiontab: { 3 | header: ({ props }) => ({ 4 | class: [ 5 | // Sizing 6 | 'pt-6 pb-0', 7 | 'mt-6', 8 | 9 | // Shape 10 | 'border-x-0 border-b-0', 11 | 12 | // Color 13 | 'border border-surface-200 dark:border-surface-700', 14 | 15 | // State 16 | { 'select-none pointer-events-none cursor-default opacity-60': props?.disabled } 17 | ] 18 | }), 19 | headerAction: ({ context }) => ({ 20 | class: [ 21 | //Font 22 | 'font-semibold', 23 | 'leading-7', 24 | 25 | // Alignments 26 | 'flex items-center justify-between flex-row-reverse', 27 | 'relative', 28 | 29 | // Shape 30 | 'rounded-md', 31 | 32 | // Color 33 | 'bg-transparent', 34 | 'text-surface-900 dark:text-surface-0', 35 | 36 | // States 37 | 'focus:outline-none focus:outline-offset-0 focus-visible:ring-2 focus-visible:ring-primary-600 ring-inset dark:focus-visible:ring-primary-500', // Focus 38 | 39 | // Misc 40 | 'cursor-pointer no-underline select-none' 41 | ] 42 | }), 43 | headerIcon: { 44 | class: 'inline-block ml-2' 45 | }, 46 | headerTitle: { 47 | class: 'leading-7' 48 | }, 49 | content: { 50 | class: [ 51 | // Font 52 | 'leading-7', 53 | 54 | // Spacing 55 | 'pr-12 pt-2', 56 | 57 | // Color 58 | 'text-surface-600 dark:text-surface-0/70' 59 | ] 60 | }, 61 | transition: { 62 | enterFromClass: 'max-h-0', 63 | enterActiveClass: 'overflow-hidden transition-[max-height] duration-1000 ease-[cubic-bezier(0.42,0,0.58,1)]', 64 | enterToClass: 'max-h-[1000px]', 65 | leaveFromClass: 'max-h-[1000px]', 66 | leaveActiveClass: 'overflow-hidden transition-[max-height] duration-[450ms] ease-[cubic-bezier(0,1,0,1)]', 67 | leaveToClass: 'max-h-0' 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /scripts/triage/mysql-triage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pull the latest MySQL image 4 | # Use same as in test https://github.com/damms005/devdb-vscode/blob/84d2bd86595db2ecb21c6ffbe8b27b1459bebd92/src/test/suite/engines/mysql.test.ts#L13 5 | docker pull mysql:8.0.31 6 | 7 | # Check if container already exists 8 | if [ "$(docker ps -a --filter 'name=^/mysql-devdb-triage$' --format '{{.Names}}')" == "mysql-devdb-triage" ]; then 9 | echo "Container exists. Starting mysql-devdb-triage if not already running..." 10 | docker start mysql-devdb-triage 11 | else 12 | echo "Container does not exist. Creating a new mysql-devdb-triage container..." 13 | docker run --name mysql-devdb-triage -e MYSQL_ROOT_PASSWORD=mysecretpassword -p 2222:3306 -d mysql:8.0.31 14 | fi 15 | 16 | # Wait for the database to start 17 | echo "Waiting for MySQL to start..." 18 | until docker exec mysql-devdb-triage mysqladmin ping -h "localhost" --silent; do 19 | sleep 1 20 | done 21 | 22 | # Create a sample database and table 23 | docker exec -i mysql-devdb-triage mysql -uroot -pmysecretpassword -P 2222 << EOF 24 | CREATE DATABASE IF NOT EXISTS sample_db; 25 | USE sample_db; 26 | 27 | CREATE TABLE IF NOT EXISTS book ( 28 | id INT AUTO_INCREMENT PRIMARY KEY, 29 | title VARCHAR(255) NOT NULL, 30 | author VARCHAR(255) NOT NULL, 31 | published_date DATE, 32 | isbn VARCHAR(13), 33 | pages INT, 34 | available BOOLEAN DEFAULT TRUE 35 | ); 36 | INSERT INTO book (title, author, published_date, isbn, pages, available) VALUES 37 | ('The Great Gatsby', 'F. Scott Fitzgerald', '1925-04-10', '9780743273565', 180, TRUE), 38 | ('1984', 'George Orwell', '1949-06-08', '9780451524935', 328, FALSE), 39 | ('To Kill a Mockingbird', 'Harper Lee', '1960-07-11', '9780061120084', 281, TRUE), 40 | ('Pride and Prejudice', 'Jane Austen', '1813-01-28', '9781503290563', 279, TRUE), 41 | ('The Catcher in the Rye', 'J.D. Salinger', '1951-07-16', '9780316769488', 214, FALSE); 42 | 43 | EOF 44 | 45 | echo "MySQL triage setup complete." 46 | 47 | echo "Example connection details:" 48 | 49 | cat << EXAMPLE_CONNECTION 50 | { 51 | "host" : "localhost", 52 | "port" : 2222, 53 | "username" : "root", 54 | "password" : "mysecretpassword", 55 | "database" : "sample_db" 56 | } 57 | EXAMPLE_CONNECTION 58 | 59 | echo "To stop the MySQL container, run the following command:" 60 | echo "docker stop mysql-devdb-triage && docker rm mysql-devdb-triage" 61 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/togglebutton/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | 'relative', 5 | 6 | // Alignment 7 | 'inline-flex', 8 | 'align-bottom', 9 | 10 | // Misc 11 | 'cursor-pointer', 12 | 'select-none' 13 | ] 14 | }, 15 | box: ({ props }) => ({ 16 | class: [ 17 | // Alignments 18 | 'items-center inline-flex flex-1 text-center align-bottom justify-center', 19 | 20 | // Sizes & Spacing 21 | 'px-2.5 py-1.5', 22 | 'text-sm', 23 | 24 | // Shapes 25 | 'rounded-md shadow-sm', 26 | 27 | // Colors 28 | 'text-surface-700 dark:text-white/80', 29 | 'ring-1 ring-surface-200 dark:ring-surface-700', 30 | { 31 | 'bg-surface-0 dark:bg-surface-900 ': !props.modelValue, 32 | 'bg-surface-100 dark:bg-surface-700': props.modelValue 33 | }, 34 | 35 | // States 36 | 'peer-hover:bg-surface-200 dark:peer-hover:bg-surface-600/80', 37 | { 38 | 'peer-focus-visible:ring-2 peer-focus-visible:ring-inset peer-focus-visible:ring-primary-500 dark:peer-focus-visible:ring-primary-400': !props.disabled 39 | }, 40 | 41 | // Transitions 42 | 'transition-all duration-200', 43 | 44 | // Misc 45 | { 'cursor-pointer': !props.disabled, 'opacity-60 select-none pointer-events-none cursor-default': props.disabled } 46 | ] 47 | }), 48 | label: { 49 | class: 'font-semibold text-center w-full' 50 | }, 51 | input: { 52 | class: [ 53 | 'peer', 54 | 55 | // Size 56 | 'w-full ', 57 | 'h-full', 58 | 59 | // Position 60 | 'absolute', 61 | 'top-0 left-0', 62 | 'z-10', 63 | 64 | // Spacing 65 | 'p-0', 66 | 'm-0', 67 | 68 | // Shape 69 | 'opacity-0', 70 | 'rounded-md', 71 | 'outline-none', 72 | 'border border-surface-200 dark:border-surface-700', 73 | 74 | // Misc 75 | 'appareance-none', 76 | 'cursor-pointer' 77 | ] 78 | }, 79 | icon: { 80 | class: [' mr-2', 'text-surface-700 dark:text-white/80'] 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /snippets/devdbrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "DevDB MySQL Configuration": { 3 | "prefix": "devdb mysql", 4 | "body": [ 5 | "{", 6 | "\t\"name\": \"${1:My MySQL Connection}\",", 7 | "\t\"type\": \"mysql\",", 8 | "\t\"host\": \"${2:localhost}\",", 9 | "\t\"port\": ${3:3306},", 10 | "\t\"username\": \"${4:root}\",", 11 | "\t\"password\": \"${5:password}\",", 12 | "\t\"database\": \"${6:database_name}\"", 13 | "}" 14 | ], 15 | "description": "Add a new MySQL database configuration" 16 | }, 17 | "DevDB MariaDB Configuration": { 18 | "prefix": "devdb mariadb", 19 | "body": [ 20 | "{", 21 | "\t\"name\": \"${1:My MariaDB Connection}\",", 22 | "\t\"type\": \"mariadb\",", 23 | "\t\"host\": \"${2:localhost}\",", 24 | "\t\"port\": ${3:3306},", 25 | "\t\"username\": \"${4:root}\",", 26 | "\t\"password\": \"${5:password}\",", 27 | "\t\"database\": \"${6:database_name}\"", 28 | "}" 29 | ], 30 | "description": "Add a new MariaDB database configuration" 31 | }, 32 | "DevDB PostgreSQL Configuration": { 33 | "prefix": "devdb postgres", 34 | "body": [ 35 | "{", 36 | "\t\"name\": \"${1:My PostgreSQL Connection}\",", 37 | "\t\"type\": \"postgres\",", 38 | "\t\"host\": \"${2:localhost}\",", 39 | "\t\"port\": ${3:5432},", 40 | "\t\"username\": \"${4:postgres}\",", 41 | "\t\"password\": \"${5:password}\",", 42 | "\t\"database\": \"${6:database_name}\"", 43 | "}" 44 | ], 45 | "description": "Add a new PostgreSQL database configuration" 46 | }, 47 | "DevDB SQLite Configuration": { 48 | "prefix": "devdb sqlite", 49 | "body": [ 50 | "{", 51 | "\t\"type\": \"sqlite\",", 52 | "\t\"path\": \"${1:/path/to/database.db3}\"", 53 | "}" 54 | ], 55 | "description": "Add a new SQLite database configuration" 56 | }, 57 | "DevDB MSSQL Configuration": { 58 | "prefix": "devdb mssql sqlserver", 59 | "body": [ 60 | "{", 61 | "\t\"name\": \"${1:My MSSQL Connection}\",", 62 | "\t\"type\": \"mssql\",", 63 | "\t\"host\": \"${2:localhost}\",", 64 | "\t\"port\": ${3:1433},", 65 | "\t\"username\": \"${4:sa}\",", 66 | "\t\"password\": \"${5:password}\",", 67 | "\t\"database\": \"${6:database_name}\",", 68 | "\t\"options\": {", 69 | "\t\t\"trustServerCertificate\": ${7:true}", 70 | "\t}", 71 | "}" 72 | ], 73 | "description": "Add a new Microsoft SQL Server database configuration" 74 | } 75 | } -------------------------------------------------------------------------------- /src/providers/sqlite/laravel-local-sqlite-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { join } from 'path'; 3 | import { DotenvParseOutput, parse } from 'dotenv'; 4 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 5 | import { SqliteEngine } from '../../database-engines/sqlite-engine'; 6 | import { fileExists, getBasePath, getWorkspaceFileContent, isComposerPhpProject } from '../../services/workspace'; 7 | 8 | export const LaravelLocalSqliteProvider: DatabaseEngineProvider = { 9 | name: 'Laravel Local SQLite (default)', 10 | type: 'sqlite', 11 | id: 'laravel-local-sqlite', 12 | description: 'Laravel with local default SQLite database', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isComposerPhpProject()) { 17 | return false; 18 | } 19 | 20 | const configContent = getWorkspaceFileContent('config', 'database.php'); 21 | if (!configContent) return false; 22 | 23 | const envFileContents = getWorkspaceFileContent('.env'); 24 | if (!envFileContents) return false; 25 | 26 | const env = parse(envFileContents); 27 | const usesSqlite = env.DB_CONNECTION == 'sqlite'; 28 | if (!usesSqlite) return false; 29 | 30 | const sqliteFilePath = await getSqliteFilePath(configContent.toString(), env) 31 | 32 | try { 33 | this.engine = new SqliteEngine(sqliteFilePath); 34 | } catch (error) { 35 | vscode.window.showErrorMessage(`SQLite file error ${sqliteFilePath}: ${String(error)}`) 36 | return false 37 | } 38 | 39 | return (await this.engine.isOkay()) 40 | }, 41 | 42 | reconnect(): Promise { 43 | return this.canBeUsedInCurrentWorkspace() 44 | }, 45 | 46 | async getDatabaseEngine(): Promise { 47 | return this.engine 48 | } 49 | } 50 | 51 | async function getSqliteFilePath(configContent: string, envFileContent: DotenvParseOutput): Promise { 52 | // Match /env('DB_DATABASE', database_path('database.sqlite'))/ irrespective of whitespace 53 | const databasePathRegex = /env\(\s*['"]DB_DATABASE['"]\s*,\s*database_path\(\s*['"]database\.sqlite['"]\s*\)\s*\)/; 54 | if (!databasePathRegex.test(configContent)) return ''; 55 | 56 | const databasePathDefinedInEnv = envFileContent.DB_DATABASE; 57 | if (databasePathDefinedInEnv) return databasePathDefinedInEnv; 58 | 59 | const workspacePath = getBasePath() 60 | if (!workspacePath) return '' 61 | 62 | const databaseFilePath = join(workspacePath, 'database', 'database.sqlite'); 63 | 64 | const exists = await fileExists(databaseFilePath) 65 | if (!exists) return ''; 66 | 67 | return databaseFilePath 68 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/radiobutton/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | 'relative', 5 | 6 | // Flexbox & Alignment 7 | 'inline-flex', 8 | 'align-bottom', 9 | 10 | // Size 11 | 'w-4 h-4', 12 | 13 | // Misc 14 | 'cursor-default', 15 | 'select-none' 16 | ] 17 | }, 18 | box: ({ props }) => ({ 19 | class: [ 20 | // Flexbox 21 | 'flex justify-center items-center', 22 | 23 | // Size 24 | 'w-4 h-4', 25 | 'text-sm', 26 | 'font-medium', 27 | 28 | // Shape 29 | 'border-2', 30 | 'rounded-full', 31 | 32 | // Transition 33 | 'transition duration-200 ease-in-out', 34 | 35 | // Colors 36 | { 37 | 'text-surface-700 dark:text-white/80': props.value !== props.modelValue && props.value !== undefined, 38 | 'bg-surface-0 dark:bg-surface-900': props.value !== props.modelValue && props.value !== undefined, 39 | 'border-surface-300 dark:border-surface-700': props.value !== props.modelValue && props.value !== undefined, 40 | 'border-primary-500 dark:border-primary-400': props.value == props.modelValue && props.value !== undefined 41 | }, 42 | 43 | // States 44 | { 45 | 'outline-none outline-offset-0': !props.disabled, 46 | 'peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-surface-0 dark:focus-visible:ring-offset-surface-800 peer-focus-visible:ring-primary-500 dark:peer-focus-visible:ring-primary-400': 47 | !props.disabled, 48 | 'opacity-60 cursor-default': props.disabled 49 | } 50 | ] 51 | }), 52 | input: { 53 | class: [ 54 | 'peer', 55 | 56 | // Size 57 | 'w-full ', 58 | 'h-full', 59 | 60 | // Position 61 | 'absolute', 62 | 'top-0 left-0', 63 | 'z-10', 64 | 65 | // Spacing 66 | 'p-0', 67 | 'm-0', 68 | 69 | // Shape 70 | 'opacity-0', 71 | 'rounded-md', 72 | 'outline-none', 73 | 'border-2 border-surface-300 dark:border-surface-700', 74 | 75 | // Misc 76 | 'appareance-none', 77 | 'cursor-default' 78 | ] 79 | }, 80 | icon: { 81 | class: 'hidden' 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/message/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Spacing and Shape 5 | 'my-2 mx-0', 6 | 'rounded-md', 7 | 'ring-1 ring-inset ring-surface-200 dark:ring-surface-700 ring-offset-0', 8 | 9 | // Colors 10 | 'bg-surface-0 dark:bg-surface-800', 11 | 12 | { 13 | 'text-blue-500 dark:text-blue-300': props.severity == 'info', 14 | 'text-green-500 dark:text-green-300': props.severity == 'success', 15 | 'text-orange-500 dark:text-orange-300': props.severity == 'warn', 16 | 'text-red-500 dark:text-red-300': props.severity == 'error' 17 | } 18 | ] 19 | }), 20 | wrapper: { 21 | class: [ 22 | // Flexbox 23 | 'flex items-center', 24 | 25 | // Spacing 26 | 'p-4' 27 | ] 28 | }, 29 | icon: { 30 | class: [ 31 | // Sizing and Spacing 32 | 'w-5 h-5', 33 | 'mr-3 shrink-0' 34 | ] 35 | }, 36 | text: { 37 | class: [ 38 | // Font and Text 39 | 'text-sm leading-none', 40 | 'font-medium' 41 | ] 42 | }, 43 | button: ({ props }) => ({ 44 | class: [ 45 | // Flexbox 46 | 'flex items-center justify-center', 47 | 48 | // Size 49 | 'w-6 h-6', 50 | 51 | // Spacing and Misc 52 | 'ml-auto relative', 53 | 54 | // Shape 55 | 'rounded-full', 56 | 57 | // Colors 58 | 'bg-transparent', 59 | 'text-surface-700 dark:text-surface-0/80', 60 | 61 | // Transitions 62 | 'transition duration-200 ease-in-out', 63 | 64 | // States 65 | 'hover:bg-surface-100 dark:hover:bg-surface-700', 66 | 'outline-none focus:ring-1 focus:ring-inset', 67 | 'focus:ring-primary-500 dark:focus:ring-primary-400', 68 | 69 | // Misc 70 | 'overflow-hidden' 71 | ] 72 | }), 73 | closeicon: { 74 | class: [ 75 | // Sizing and Spacing 76 | 'w-3 h-3', 77 | 'shrink-0' 78 | ] 79 | }, 80 | transition: { 81 | enterFromClass: 'opacity-0', 82 | enterActiveClass: 'transition-opacity duration-300', 83 | leaveFromClass: 'max-h-40', 84 | leaveActiveClass: 'overflow-hidden transition-all duration-300 ease-in', 85 | leaveToClass: 'max-h-0 opacity-0 !m-0' 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/services/go-to-table.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DevDbViewProvider } from "../devdb-view-provider"; 3 | import { autoConnectProvider, getAvailableProviders, getConnectedDatabase } from "./messenger"; 4 | import { DatabaseEngine } from '../types'; 5 | import { logToOutput } from './output-service'; 6 | 7 | export async function goToTable(devDbViewProvider: DevDbViewProvider | undefined) { 8 | 9 | logToOutput('Attempting to open table', 'Go to Table') 10 | 11 | if (!devDbViewProvider) { 12 | logToOutput('No DevDB view provider found. Please connect to a database first.', 'Go to Table') 13 | vscode.window.showErrorMessage('No DevDB view provider found. Please connect to a database first.'); 14 | return; 15 | } 16 | 17 | const database = await getDatabase(devDbViewProvider) 18 | 19 | if (!database) { 20 | logToOutput('No database connection found. Please connect to one first.', 'Go to Table') 21 | vscode.window.showErrorMessage('No database connection found. Please connect to one first.'); 22 | vscode.commands.executeCommand('devdb.focus'); 23 | return; 24 | } 25 | 26 | const tables = await database.getTables(); 27 | 28 | if (!tables || tables.length === 0) { 29 | logToOutput('No tables found in the currently connected database', 'Go to Table') 30 | vscode.window.showErrorMessage('No tables found in the currently connected database'); 31 | return; 32 | } 33 | 34 | const selectedTable = await vscode.window.showQuickPick(tables, { 35 | placeHolder: 'Select a table to open', 36 | title: 'Go to Table' 37 | }); 38 | 39 | if (selectedTable) { 40 | logToOutput(`Selected table: ${selectedTable}`, 'Go to Table') 41 | devDbViewProvider.setActiveTable(selectedTable); 42 | } 43 | } 44 | 45 | export async function getDatabase(devDbViewProvider: DevDbViewProvider): Promise { 46 | let database: DatabaseEngine | null = await getConnectedDatabase() 47 | 48 | if (database) { 49 | return database; 50 | } 51 | 52 | const nonDefaultProviders = (await getAvailableProviders()).filter((provider) => !provider.isDefault); 53 | 54 | if (nonDefaultProviders.length !== 1) { 55 | vscode.window.showErrorMessage(`This project has ${nonDefaultProviders.length} available database providers and we do not know which one to use. Please connect to one first.`); 56 | vscode.commands.executeCommand('devdb.focus'); 57 | return null; 58 | } 59 | 60 | /** 61 | * This is just to ensure the UI is consistent with all the auto-connection 62 | * magic we are doing here in the backend 63 | */ 64 | await autoConnectProvider(devDbViewProvider, nonDefaultProviders[0]); 65 | 66 | return await getConnectedDatabase() 67 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/divider/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Flex and Position 5 | 'flex relative', 6 | { 'justify-center': props.layout == 'vertical' }, 7 | { 'items-center': props.layout == 'vertical' }, 8 | { 9 | 'justify-start': props?.align == 'left' && props.layout == 'horizontal', 10 | 'justify-center': props?.align == 'center' && props.layout == 'horizontal', 11 | 'justify-end': props?.align == 'right' && props.layout == 'horizontal', 12 | 'items-center': props?.align == 'top' && props.layout == 'vertical', 13 | 'items-start': props?.align == 'center' && props.layout == 'vertical', 14 | 'items-end': props?.align == 'bottom' && props.layout == 'vertical' 15 | }, 16 | 17 | // Spacing 18 | { 19 | 'my-5 mx-0 py-0 px-5': props.layout == 'horizontal', 20 | 'mx-4 md:mx-5 py-5': props.layout == 'vertical' 21 | }, 22 | 23 | // Size 24 | { 25 | 'w-full': props.layout == 'horizontal', 26 | 'min-h-full': props.layout == 'vertical' 27 | }, 28 | 29 | // Before: Line 30 | 'before:block', 31 | 32 | // Position 33 | { 34 | 'before:absolute before:left-0 before:top-1/2': props.layout == 'horizontal', 35 | 'before:absolute before:left-1/2 before:top-0 before:transform before:-translate-x-1/2': props.layout == 'vertical' 36 | }, 37 | 38 | // Size 39 | { 40 | 'before:w-full': props.layout == 'horizontal', 41 | 'before:min-h-full': props.layout == 'vertical' 42 | }, 43 | 44 | // Shape 45 | { 46 | 'before:border-solid': props.type == 'solid', 47 | 'before:border-dotted': props.type == 'dotted', 48 | 'before:border-dashed': props.type == 'dashed' 49 | }, 50 | 51 | // Color 52 | { 53 | 'before:border-t before:border-surface-200 before:dark:border-surface-600': props.layout == 'horizontal', 54 | 'before:border-l before:border-surface-200 before:dark:border-surface-600': props.layout == 'vertical' 55 | } 56 | ] 57 | }), 58 | content: { 59 | class: [ 60 | // Space and Position 61 | 'p-2 z-10', 62 | 63 | // Color 64 | 'bg-surface-0 dark:bg-surface-800' 65 | ] 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/tabmenu/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: 'overflow-x-auto' 4 | }, 5 | menu: { 6 | class: [ 7 | // Flexbox 8 | 'flex flex-1', 9 | 10 | // Spacing 11 | 'list-none', 12 | 'p-0 m-0', 13 | 14 | // Colors 15 | 'bg-surface-0 dark:bg-surface-800', 16 | 'border-b border-surface-200 dark:border-surface-700', 17 | 'text-surface-900 dark:text-surface-0/80' 18 | ] 19 | }, 20 | menuitem: { 21 | class: 'mr-0' 22 | }, 23 | action: ({ context, state }) => ({ 24 | class: [ 25 | 'relative', 26 | 27 | // Font 28 | 'font-medium', 29 | 'text-md', 30 | 31 | // Flexbox and Alignment 32 | 'flex items-center', 33 | 34 | // Spacing 35 | 'py-4 px-3', 36 | '-mb-[1px]', 37 | 38 | // Shape 39 | 'border-b-2', 40 | 'rounded-t-md', 41 | 42 | // Colors and Conditions 43 | { 44 | 'border-surface-200 dark:border-surface-700': state.d_activeIndex !== context.index, 45 | 'bg-surface-0 dark:bg-surface-800': state.d_activeIndex !== context.index, 46 | 'text-surface-700 dark:text-surface-0/80': state.d_activeIndex !== context.index, 47 | 48 | 'bg-surface-0 dark:bg-surface-800': state.d_activeIndex === context.index, 49 | 'border-primary-500 dark:border-primary-400': state.d_activeIndex === context.index, 50 | 'text-primary-500 dark:text-primary-400': state.d_activeIndex === context.index 51 | }, 52 | 53 | // States 54 | 'focus-visible:outline-none focus-visible:outline-offset-0 focus-visible:ring-2 focus-visible:ring-inset', 55 | 'focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400', 56 | { 57 | 'hover:bg-surface-0 dark:hover:bg-surface-800/80': state.d_activeIndex !== context.index, 58 | 'hover:border-surface-400 dark:hover:border-surface-600': state.d_activeIndex !== context.index, 59 | 'hover:text-surface-900 dark:hover:text-surface-0': state.d_activeIndex !== context.index 60 | }, 61 | 62 | // Transitions 63 | 'transition-all duration-200', 64 | 65 | // Misc 66 | 'cursor-pointer select-none text-decoration-none', 67 | 'overflow-hidden', 68 | 'user-select-none', 69 | 'whitespace-nowrap' 70 | ] 71 | }), 72 | icon: { 73 | class: 'mr-2' 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /scripts/triage/postgres-triage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Pull the latest PostgreSQL image 4 | # Use same as in test https://github.com/damms005/devdb-vscode/blob/eec97b76b630a2fc26b6bfaba17585a2c4dd1fd2/src/test/suite/engines/postgres.test.ts#L13 5 | docker pull postgres:13.3-alpine 6 | 7 | # Check if container exists 8 | if [ "$(docker ps -a --filter 'name=^/postgres-devdb-triage$' --format '{{.Names}}')" == "postgres-devdb-triage" ]; then 9 | echo "Container exists. Starting postgres-devdb-triage if not already running..." 10 | docker start postgres-devdb-triage 11 | else 12 | echo "Container does not exist. Creating a new postgres-devdb-triage container..." 13 | docker run --name postgres-devdb-triage -e POSTGRES_PASSWORD=mysecretpassword -p 3333:5432 -d postgres:13.3-alpine 14 | fi 15 | 16 | # Wait for the database to start 17 | echo "Waiting for PostgreSQL to start..." 18 | until docker exec postgres-devdb-triage pg_isready -U postgres; do 19 | sleep 1 20 | done 21 | 22 | # Create a sample database and table 23 | docker exec -i postgres-devdb-triage psql -U postgres << EOF 24 | CREATE DATABASE sample_db; 25 | \c sample_db 26 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 27 | 28 | CREATE TABLE book ( 29 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 30 | title VARCHAR(255) NOT NULL, 31 | author VARCHAR(255) NOT NULL, 32 | published_date DATE, 33 | isbn VARCHAR(13), 34 | pages INT, 35 | available BOOLEAN DEFAULT TRUE, 36 | "user" UUID, 37 | "order" INT 38 | ); 39 | INSERT INTO book (title, author, published_date, isbn, pages, available, "user", "order") VALUES 40 | ('The Great Gatsby', 'F. Scott Fitzgerald', '1925-04-10', '9780743273565', 180, TRUE, uuid_generate_v4(), 1), 41 | ('1984', 'George Orwell', '1949-06-08', '9780451524935', 328, FALSE, uuid_generate_v4(), 2), 42 | ('To Kill a Mockingbird', 'Harper Lee', '1960-07-11', '9780061120084', 281, TRUE, uuid_generate_v4(), 3), 43 | ('Pride and Prejudice', 'Jane Austen', '1813-01-28', '9781503290563', 279, TRUE, uuid_generate_v4(), 4), 44 | ('The Catcher in the Rye', 'J.D. Salinger', '1951-07-16', '9780316769488', 214, FALSE, uuid_generate_v4(), 5); 45 | 46 | EOF 47 | 48 | echo "PostgreSQL triage setup complete." 49 | 50 | echo "Example connection details:" 51 | 52 | cat << EXAMPLE_CONNECTION 53 | { 54 | "host" : "localhost", 55 | "port" : 3333, 56 | "username" : "postgres", 57 | "password" : "mysecretpassword", 58 | "database" : "sample_db" 59 | } 60 | EXAMPLE_CONNECTION 61 | 62 | echo "You can connect to it from inside the container like: 'psql sample_db postgres'" 63 | echo "To stop the PostgreSQL container, run the following command:" 64 | echo "docker stop postgres-devdb-triage && docker rm postgres-devdb-triage" 65 | -------------------------------------------------------------------------------- /src/providers/postgres/rails-postgres-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import knex from 'knex'; 3 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 4 | import { PostgresEngine } from '../../database-engines/postgres-engine'; 5 | import { isRailsProject, parseDatabaseYml, getRailsEnv } from '../../services/rails/rails-core'; 6 | import { log } from '../../services/logging-service'; 7 | 8 | export const RailsPostgresProvider: DatabaseEngineProvider = { 9 | name: 'Rails PostgreSQL', 10 | type: 'postgres', 11 | id: 'rails-postgres', 12 | description: 'Rails PostgreSQL with config/database.yml', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isRailsProject()) { 17 | log('Rails Postgres Provider', 'Not a Rails project'); 18 | return false; 19 | } 20 | 21 | try { 22 | const config = await parseDatabaseYml(getRailsEnv()); 23 | if (!config) { 24 | log('Rails Postgres Provider', 'Failed to parse database configuration using Rails runner'); 25 | return false; 26 | } 27 | 28 | if (config.adapter !== 'postgres') { 29 | log('Rails Postgres Provider', `Database adapter is ${config.adapter}, not postgres`); 30 | return false; 31 | } 32 | 33 | const connection = knex({ 34 | client: 'pg', 35 | connection: { 36 | host: config.host, 37 | port: config.port, 38 | user: config.username, 39 | password: config.password, 40 | database: config.database, 41 | pool: config.pool ? { min: 0, max: config.pool } : undefined, 42 | }, 43 | acquireConnectionTimeout: config.timeout || 60000, 44 | }); 45 | 46 | if (!connection) { 47 | log('Rails Postgres Provider', 'Failed to create Knex connection'); 48 | return false; 49 | } 50 | 51 | this.engine = new PostgresEngine(connection); 52 | } catch (error) { 53 | const errorMessage = `Rails PostgreSQL connection error: ${String(error)}`; 54 | log('Rails Postgres Provider', errorMessage); 55 | vscode.window.showErrorMessage(errorMessage); 56 | return false; 57 | } 58 | 59 | try { 60 | const isOkay = await this.engine.isOkay(); 61 | if (!isOkay) { 62 | log('Rails Postgres Provider', 'Database connection validation failed'); 63 | } 64 | return isOkay; 65 | } catch (error) { 66 | const errorMessage = `Rails PostgreSQL connection validation error: ${String(error)}`; 67 | log('Rails Postgres Provider', errorMessage); 68 | vscode.window.showErrorMessage(errorMessage); 69 | return false; 70 | } 71 | }, 72 | 73 | reconnect(): Promise { 74 | return this.canBeUsedInCurrentWorkspace(); 75 | }, 76 | 77 | async getDatabaseEngine(): Promise { 78 | return this.engine; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /src/providers/mysql/adonis-mysql-provider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 4 | import { log } from '../../services/logging-service'; 5 | import { MysqlEngine } from '../../database-engines/mysql-engine'; 6 | import { getConnectionInEnvFile } from '../../services/adonis/env-file-parser'; 7 | import { isAdonisProject } from '../../services/adonis/adonis-core'; 8 | import { logToOutput } from '../../services/output-service'; 9 | import { getBasePath } from '../../services/workspace'; 10 | 11 | export const AdonisMysqlProvider: DatabaseEngineProvider = { 12 | name: 'Adonis MySQL (Lucid ORM)', 13 | type: 'mysql', 14 | id: 'adonis-lucid-mysql', 15 | description: 'MySQL database for Adonis.js projects (Lucid ORM)', 16 | engine: undefined, 17 | 18 | async canBeUsedInCurrentWorkspace(): Promise { 19 | try { 20 | const workspaceRoot = getBasePath(); 21 | if (!workspaceRoot) { 22 | log('Adonis MySQL', 'No workspace root found'); 23 | return false; 24 | } 25 | 26 | const databaseTsPath = path.join(workspaceRoot, 'config', 'database.ts'); 27 | const databaseJsPath = path.join(workspaceRoot, 'config', 'database.js'); 28 | 29 | if (!isAdonisProject()) { 30 | logToOutput('Not an AdonisJS project', 'MySQL AdonisJS') 31 | return false; 32 | } 33 | 34 | const configPath = fs.existsSync(databaseTsPath) ? databaseTsPath : databaseJsPath; 35 | const configContent = fs.readFileSync(configPath, 'utf8'); 36 | 37 | if (!configContent.includes('mysql') || configContent.includes('postgresql') || configContent.includes('postgres')) { 38 | log('Adonis MySQL', 'MySQL not configured in Adonis database config'); 39 | return false; 40 | } 41 | 42 | const connection = await getConnectionInEnvFile('mysql', 'mysql2'); 43 | if (!connection) { 44 | log('Adonis MySQL', 'Could not extract MySQL connection details from .env file'); 45 | return false; 46 | } 47 | 48 | this.engine = new MysqlEngine(connection); 49 | return this.engine.isOkay(); 50 | } catch (error) { 51 | log('Adonis postgres', 'Error checking if Adonis MySQL provider can be used', error); 52 | return false; 53 | } 54 | }, 55 | 56 | reconnect(): Promise { 57 | return this.canBeUsedInCurrentWorkspace() 58 | }, 59 | 60 | async getDatabaseEngine(): Promise { 61 | return this.engine 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/providers/postgres/django-postgres-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import knex from 'knex'; 3 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 4 | import { PostgresEngine } from '../../database-engines/postgres-engine'; 5 | import { isDjangoProject, getDatabaseConfig, getDjangoDbAlias } from '../../services/django/django-core'; 6 | import { log } from '../../services/logging-service'; 7 | 8 | export const DjangoPostgresProvider: DatabaseEngineProvider = { 9 | name: 'Django PostgreSQL', 10 | type: 'postgres', 11 | id: 'django-postgres', 12 | description: 'Django PostgreSQL with settings.py', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isDjangoProject()) { 17 | log('Django Postgres Provider', 'Not a Django project'); 18 | return false; 19 | } 20 | 21 | try { 22 | const config = await getDatabaseConfig(getDjangoDbAlias()); 23 | if (!config) { 24 | log('Django Postgres Provider', 'Failed to parse database configuration using Django shell'); 25 | return false; 26 | } 27 | 28 | if (config.adapter !== 'postgres') { 29 | log('Django Postgres Provider', `Database adapter is ${config.adapter}, not postgres`); 30 | return false; 31 | } 32 | 33 | const connection = knex({ 34 | client: 'pg', 35 | connection: { 36 | host: config.host, 37 | port: config.port, 38 | user: config.username, 39 | password: config.password, 40 | database: config.database, 41 | pool: config.pool ? { min: 0, max: config.pool } : undefined, 42 | }, 43 | acquireConnectionTimeout: config.timeout || 60000, 44 | }); 45 | 46 | if (!connection) { 47 | log('Django Postgres Provider', 'Failed to create Knex connection'); 48 | return false; 49 | } 50 | 51 | this.engine = new PostgresEngine(connection); 52 | } catch (error) { 53 | const errorMessage = `Django PostgreSQL connection error: ${String(error)}`; 54 | log('Django Postgres Provider', errorMessage); 55 | vscode.window.showErrorMessage(errorMessage); 56 | return false; 57 | } 58 | 59 | try { 60 | const isOkay = await this.engine.isOkay(); 61 | if (!isOkay) { 62 | log('Django Postgres Provider', 'Database connection validation failed'); 63 | } 64 | return isOkay; 65 | } catch (error) { 66 | const errorMessage = `Django PostgreSQL connection validation error: ${String(error)}`; 67 | log('Django Postgres Provider', errorMessage); 68 | vscode.window.showErrorMessage(errorMessage); 69 | return false; 70 | } 71 | }, 72 | 73 | reconnect(): Promise { 74 | return this.canBeUsedInCurrentWorkspace(); 75 | }, 76 | 77 | async getDatabaseEngine(): Promise { 78 | return this.engine; 79 | } 80 | }; -------------------------------------------------------------------------------- /src/services/adonis/env-file-parser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import knexlib from "knex"; 3 | import { getEnvFileValue } from "./adonis-core"; 4 | import { getConnectionFor } from "../connector"; 5 | import { KnexClientType, LaravelConnection } from '../../types'; 6 | import { log } from '../logging-service'; 7 | import { reportError } from '../initialization-error-service'; 8 | 9 | export async function getConnectionInEnvFile(connection: LaravelConnection, dialect: KnexClientType): Promise { 10 | log('Adonis env file parser', 'Fetching connection details from .env file. Laravel connection: ', connection); 11 | const host = await getHost(); 12 | const username = await getEnvFileValue('DB_USER') || ''; 13 | const password = await getEnvFileValue('DB_PASSWORD') || ''; 14 | const database = await getEnvFileValue('DB_DATABASE'); 15 | log('Adonis env file parser', `Laravel/${dialect} connection details: host=${host}, username=${username}, database=${database ? String(database[0]) + '*****' : ''}`); 16 | 17 | if (!database) { 18 | reportError('Adonis env file parser: missing database name in .env file') 19 | return 20 | } 21 | 22 | if (dialect !== 'mysql2' && dialect !== 'postgres') { 23 | vscode.window.showErrorMessage(`No support for '${dialect}' in Laravel Sail yet`) 24 | 25 | log('Adonis env file parser', `Error connecting using host configured in .env file. Conn:`, connection); 26 | return; 27 | } 28 | 29 | let portOrConnection = await getPort(); 30 | 31 | if (!database || !portOrConnection) { 32 | log('Adonis env file parser', `Missing database or port: database=${database ? String(database[0]) + '*****' : ''}, port=${portOrConnection}`); 33 | return; 34 | } 35 | 36 | log('Adonis env file parser', `Laravel/${dialect} connection details:`, host, portOrConnection, username, database); 37 | if (typeof portOrConnection === 'object') { 38 | return portOrConnection 39 | } 40 | 41 | try { 42 | return await connectUsingHostConfiguredInEnvFile(dialect, host, portOrConnection, username, password, database) 43 | } catch (error) { 44 | return 45 | } 46 | } 47 | 48 | async function connectUsingHostConfiguredInEnvFile(dialect: KnexClientType, host: string, port: number, username: string, password: string, database: string): Promise { 49 | return await getConnectionFor('Adonis env file parser', dialect, host, port, username, password, database) 50 | } 51 | 52 | async function getHost() { 53 | const localhost = '127.0.0.1' 54 | 55 | return (await getEnvFileValue('DB_HOST')) || localhost 56 | } 57 | 58 | async function getPort(): Promise { 59 | const portInEnvFile = await getEnvFileValue('DB_PORT') 60 | return parseInt(portInEnvFile || '3306') 61 | } 62 | -------------------------------------------------------------------------------- /src/services/mcp/no-vscode/port-manager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import logger from "./logger" 3 | import { MCP_CONFIG_DIR, MCP_CONFIG_FILE } from './config'; 4 | 5 | interface McpConfig { 6 | [workspaceId: string]: number; 7 | } 8 | 9 | function ensureConfigDir(): void { 10 | if (!fs.existsSync(MCP_CONFIG_DIR)) { 11 | fs.mkdirSync(MCP_CONFIG_DIR, { recursive: true }); 12 | } 13 | } 14 | 15 | function readMcpConfig(): McpConfig { 16 | try { 17 | if (fs.existsSync(MCP_CONFIG_FILE)) { 18 | const content = fs.readFileSync(MCP_CONFIG_FILE, 'utf8'); 19 | return JSON.parse(content); 20 | } 21 | } catch (error) { 22 | logger.error('Failed to read MCP config file:', error); 23 | } 24 | return {}; 25 | } 26 | 27 | function writeMcpConfig(config: McpConfig): void { 28 | try { 29 | ensureConfigDir(); 30 | fs.writeFileSync(MCP_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); 31 | } catch (error) { 32 | logger.error('Failed to write MCP config file:', error); 33 | throw error; 34 | } 35 | } 36 | 37 | export function savePort(port: number, workspaceId: string): void { 38 | try { 39 | const config = readMcpConfig(); 40 | 41 | for (const existingWorkspaceId in config) { 42 | if (config[existingWorkspaceId] === port) { 43 | delete config[existingWorkspaceId]; 44 | logger.info(`DevDB: Removed port ${port} from workspace ${existingWorkspaceId}`); 45 | } 46 | } 47 | 48 | config[workspaceId] = port; 49 | writeMcpConfig(config); 50 | } catch (error) { 51 | logger.error('Failed to save port to config:', error); 52 | throw error; 53 | } 54 | } 55 | 56 | export function getPort(): number | null { 57 | try { 58 | if (!process.env.WORKSPACE_ID) { 59 | throw new Error('WORKSPACE_ID environment variable is required for MCP server process'); 60 | } 61 | 62 | const workspaceId = process.env.WORKSPACE_ID; 63 | const config = readMcpConfig(); 64 | const port = config[workspaceId]; 65 | 66 | if (port) { 67 | logger.info(`DevDB: Read port ${port} for workspace ${workspaceId} from ${MCP_CONFIG_FILE}`); 68 | return port; 69 | } else { 70 | logger.info(`DevDB: No port found for workspace ${workspaceId} in ${MCP_CONFIG_FILE}`); 71 | return null; 72 | } 73 | } catch (error) { 74 | logger.error('Failed to read port from config:', error); 75 | return null; 76 | } 77 | } 78 | 79 | export function clearPort(workspaceId: string): void { 80 | try { 81 | const config = readMcpConfig(); 82 | if (config[workspaceId]) { 83 | delete config[workspaceId]; 84 | writeMcpConfig(config); 85 | logger.info(`DevDB: Cleared port for workspace ${workspaceId}`); 86 | } 87 | } catch (error) { 88 | logger.error('Failed to clear port from config:', error); 89 | } 90 | } 91 | 92 | export function getConfigDir() { 93 | return MCP_CONFIG_DIR; 94 | } -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/inputswitch/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Alignments 5 | 'inline-flex relative', 6 | 'shrink-0', 7 | 8 | // Shape 9 | 'rounded-2xl', 10 | 11 | // Size 12 | 'h-5 w-9', 13 | 14 | // States 15 | { 16 | 'opacity-60 select-none pointer-events-none cursor-default': props.disabled 17 | } 18 | ] 19 | }), 20 | slider: ({ props }) => ({ 21 | class: [ 22 | // Position 23 | 'absolute top-0 left-0 right-0 bottom-0', 24 | 25 | // Shape 26 | 'rounded-2xl', 27 | 28 | // Before: 29 | 'before:absolute before:top-1/2', 30 | 'before:-mt-2', 31 | 'before:h-4 before:w-4', 32 | 'before:rounded-full', 33 | 'before:duration-200 before:transition before:ease-in-out', 34 | 'before:bg-surface-0 before:dark:bg-surface-900', 35 | 'before:shadow', 36 | { 'before:transform before:translate-x-4': props.modelValue == props.trueValue }, 37 | 38 | // Colors 39 | 'border-2 border-transparent', 40 | { 41 | 'bg-surface-200 dark:bg-surface-700': !(props.modelValue == props.trueValue), 42 | 'bg-primary-500 dark:bg-primary-400': props.modelValue == props.trueValue 43 | }, 44 | 45 | // States 46 | { 'peer-hover:bg-surface-300 dark:peer-hover:bg-surface-600 ': !(props.modelValue == props.trueValue) && !props.disabled }, 47 | { 'peer-hover:bg-primary-600 dark:peer-hover:bg-surface-300 ': (props.modelValue == props.trueValue) && !props.disabled }, 48 | 'peer-focus-visible:ring-2 peer-focus-visible:ring-primary-500 dark:peer-focus-visible:ring-primary-400', 49 | 50 | // Transition 51 | 'transition-colors duration-200', 52 | 53 | // Misc 54 | 'cursor-pointer' 55 | ] 56 | }), 57 | input: { 58 | class: [ 59 | 'peer', 60 | 61 | // Size 62 | 'w-full ', 63 | 'h-full', 64 | 65 | // Position 66 | 'absolute', 67 | 'top-0 left-0', 68 | 'z-10', 69 | 70 | // Spacing 71 | 'p-0', 72 | 'm-0', 73 | 74 | // Shape 75 | 'rounded', 76 | 'border', 77 | 78 | // Shape 79 | 'opacity-0', 80 | 'rounded-md', 81 | 'outline-none', 82 | 'border-2 border-surface-300 dark:border-surface-700', 83 | 84 | // Misc 85 | 'appareance-none' 86 | ] 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /src/test/suite/services/string.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { extractVariables, replaceVariables } from '../../../services/string'; 3 | 4 | describe('Variable Replacement', () => { 5 | describe('extractVariables', () => { 6 | it('should extract single object property variable', () => { 7 | const text = 'SessionInfo::where("program_id", $program->id)'; 8 | const variables = extractVariables(text); 9 | assert.deepStrictEqual(variables, ['$program->id']); 10 | }); 11 | 12 | it('should extract multiple object property variables', () => { 13 | const text = 'SessionInfo::where("program_id", $program->id)->where("type", $program->type->name)'; 14 | const variables = extractVariables(text); 15 | assert.deepStrictEqual(variables, ['$program->id', '$program->type->name']); 16 | }); 17 | 18 | it('should extract nested object property variables', () => { 19 | const text = 'SessionInfo::where("type_id", $program->type->another->thing_id)'; 20 | const variables = extractVariables(text); 21 | assert.deepStrictEqual(variables, ['$program->type->another->thing_id']); 22 | }); 23 | }); 24 | 25 | describe('replaceVariables', () => { 26 | it('should replace single object property variable', () => { 27 | const text = 'SessionInfo::where("program_id", $program->id)'; 28 | const variableValues = { 29 | '$program->id': '123' 30 | }; 31 | const result = replaceVariables(text, variableValues); 32 | assert.deepStrictEqual(result, 'SessionInfo::where("program_id", 123)'); 33 | }); 34 | 35 | it('should replace multiple object property variables', () => { 36 | const text = 'SessionInfo::where("program_id", $program->id)->where("type", $program->type->name)'; 37 | const variableValues = { 38 | '$program->id': '123', 39 | '$program->type->name': '"active"' 40 | }; 41 | const result = replaceVariables(text, variableValues); 42 | assert.deepStrictEqual(result, 'SessionInfo::where("program_id", 123)->where("type", "active")'); 43 | }); 44 | 45 | it('should replace nested object property variables', () => { 46 | const text = 'SessionInfo::where("type_id", $program->type->another->thing_id)'; 47 | const variableValues = { 48 | '$program->type->another->thing_id': '456' 49 | }; 50 | const result = replaceVariables(text, variableValues); 51 | assert.deepStrictEqual(result, 'SessionInfo::where("type_id", 456)'); 52 | }); 53 | 54 | it('should handle multiple occurrences of same variable', () => { 55 | const text = 'SessionInfo::where("program_id", $program->id)->orWhere("backup_id", $program->id)'; 56 | const variableValues = { 57 | '$program->id': '123' 58 | }; 59 | const result = replaceVariables(text, variableValues); 60 | assert.deepStrictEqual(result, 'SessionInfo::where("program_id", 123)->orWhere("backup_id", 123)'); 61 | }); 62 | }); 63 | }); -------------------------------------------------------------------------------- /src/providers/postgres/adonis-postgres-provider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 4 | import { log } from '../../services/logging-service'; 5 | import { PostgresEngine } from '../../database-engines/postgres-engine'; 6 | import { getConnectionInEnvFile } from '../../services/adonis/env-file-parser'; 7 | import { isAdonisProject } from '../../services/adonis/adonis-core'; 8 | import { logToOutput } from '../../services/output-service'; 9 | import { getBasePath } from '../../services/workspace'; 10 | 11 | export const AdonisPostgresProvider: DatabaseEngineProvider = { 12 | name: 'Adonis PostgreSQL (Lucid ORM)', 13 | type: 'postgres', 14 | id: 'adonis-lucid-postgres', 15 | description: 'PostgreSQL database for Adonis.js projects (Lucid ORM)', 16 | engine: undefined, 17 | 18 | async canBeUsedInCurrentWorkspace(): Promise { 19 | try { 20 | const workspaceRoot = getBasePath(); 21 | if (!workspaceRoot) { 22 | log('Adonis PostgreSQL', 'No workspace root found'); 23 | return false; 24 | } 25 | 26 | const databaseTsPath = path.join(workspaceRoot, 'config', 'database.ts'); 27 | const databaseJsPath = path.join(workspaceRoot, 'config', 'database.js'); 28 | 29 | if (!isAdonisProject()) { 30 | logToOutput('Not an AdonisJS project', 'Postgres AdonisJS') 31 | return false; 32 | } 33 | 34 | const configPath = fs.existsSync(databaseTsPath) ? databaseTsPath : databaseJsPath; 35 | const configContent = fs.readFileSync(configPath, 'utf8'); 36 | 37 | // Look for PostgreSQL configuration in the Adonis database config 38 | const isPgConfigured = configContent.includes('postgres') || 39 | configContent.includes('pg') || 40 | configContent.includes('postgresql'); 41 | 42 | if (!isPgConfigured) { 43 | return false; 44 | } 45 | 46 | // Extract connection details from .env file 47 | const connection = await getConnectionInEnvFile('pgsql', 'postgres'); 48 | if (!connection) { 49 | return false; 50 | } 51 | 52 | this.engine = new PostgresEngine(connection); 53 | 54 | return (await this.engine.isOkay()); 55 | } catch (error) { 56 | log('Adonis PostgreSQL', 'Could not check if Adonis PostgreSQL provider can be used:' + String(error)); 57 | return false; 58 | } 59 | }, 60 | 61 | reconnect(): Promise { 62 | return this.canBeUsedInCurrentWorkspace() 63 | }, 64 | 65 | async getDatabaseEngine(): Promise { 66 | return this.engine 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a problem you're experiencing 3 | labels: bug,unconfirmed,low priority 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to file a bug report. Before opening a bug report, please search the existing issues (both open and closed). 9 | To address this bug, please provide these information: 10 | - type: input 11 | id: extension-version 12 | attributes: 13 | label: Extension Version 14 | description: Please provide the full version of your installed DevDb. 15 | placeholder: v1.0.0 16 | validations: 17 | required: true 18 | - type: input 19 | id: os-version 20 | attributes: 21 | label: OS and Version 22 | description: Please provide your OS and version. 23 | placeholder: Ubuntu v23.10 24 | validations: 25 | required: true 26 | - type: input 27 | id: vs-code-version 28 | attributes: 29 | label: VS Code Version 30 | description: Please provide the full version of your VS Code. 31 | placeholder: v1.85.1 32 | validations: 33 | required: true 34 | - type: input 35 | id: database-and-version 36 | attributes: 37 | label: Database name and Version 38 | description: Please provide your database name and version. e.g. for MySQL, output of `mysql --version`. 39 | placeholder: mysql Ver 8.0.35 for Linux on x86_64 (MySQL Community Server - GPL) 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: reproduction-steps 44 | attributes: 45 | label: Steps to reproduce 46 | description: Which steps do we need to take to reproduce the problem? Any code examples need to be **as short as possible**, remove any code that is unrelated to the bug. **This issue will be automatically closed and not reviewed if detailed replication steps are missing.** 47 | placeholder: | 48 | Steps to reproduce the behavior: 49 | 1. Launch VS Code 50 | 2. Open a '...' 51 | 3. Click on '....' 52 | 4. Scroll to '....' 53 | 5. See ... error 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: expectation 58 | attributes: 59 | label: Expected behavior 60 | description: What did you expect to happen instead? 61 | placeholder: A clear and concise description of what you expected to happen. 62 | validations: 63 | required: true 64 | - type: textarea 65 | id: screenshots 66 | attributes: 67 | label: Screenshots 68 | description: Please add screen recording or screenshots to help explain your problem. 69 | validations: 70 | required: true 71 | - type: textarea 72 | id: additional-context 73 | attributes: 74 | label: Additional context 75 | description: Add any other context about the problem here. 76 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/checkbox/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | 'relative', 5 | 6 | // Alignment 7 | 'inline-flex', 8 | 'align-bottom', 9 | 10 | // Size 11 | 'w-4', 12 | 'h-4', 13 | 14 | // Misc 15 | 'cursor-default', 16 | 'select-none' 17 | ] 18 | }, 19 | box: ({ props, context }) => ({ 20 | class: [ 21 | // Alignment 22 | 'flex', 23 | 'items-center', 24 | 'justify-center', 25 | 26 | // Size 27 | 'w-4', 28 | 'h-4', 29 | 30 | // Shape 31 | 'rounded', 32 | 'border', 33 | 34 | // Colors 35 | 'text-surface-600', 36 | { 37 | 'border-surface-300 bg-surface-0 dark:border-surface-700 dark:bg-surface-900': !context.checked, 38 | 'border-primary-500 bg-primary-500 dark:border-primary-400 dark:bg-primary-400': context.checked 39 | }, 40 | 41 | { 42 | 'ring-2 ring-primary-500 dark:ring-primary-400': !props.disabled && context.focused, 43 | 'cursor-default opacity-60': props.disabled 44 | }, 45 | 46 | // States 47 | { 48 | 'peer-focus-visible:ring-2 peer-focus-visible:ring-primary-500 dark:peer-focus-visible:ring-primary-400': !props.disabled, 49 | 'cursor-default opacity-60': props.disabled 50 | }, 51 | 52 | // Transitions 53 | 'transition-colors', 54 | 'duration-200' 55 | ] 56 | }), 57 | input: { 58 | class: [ 59 | 'peer', 60 | 61 | // Size 62 | 'w-full ', 63 | 'h-full', 64 | 65 | // Position 66 | 'absolute', 67 | 'top-0 left-0', 68 | 'z-10', 69 | 70 | // Spacing 71 | 'p-0', 72 | 'm-0', 73 | 74 | // Shape 75 | 'rounded', 76 | 'border', 77 | 78 | // Shape 79 | 'opacity-0', 80 | 'rounded-md', 81 | 'outline-none', 82 | 'border-2 border-surface-300 dark:border-surface-700', 83 | 84 | // Misc 85 | 'appareance-none' 86 | ] 87 | }, 88 | icon: { 89 | class: [ 90 | // Font 91 | 'text-normal', 92 | 93 | // Size 94 | 'w-3', 95 | 'h-3', 96 | 97 | // Colors 98 | 'text-white dark:text-surface-900', 99 | 100 | // Transitions 101 | 'transition-all', 102 | 'duration-200' 103 | ] 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/fieldset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | 'block', 5 | 6 | // Spacing 7 | 'px-5 md:px-6 py-5', 8 | 9 | // Shape 10 | 'rounded-md rounded-lg', 11 | 12 | // Color 13 | 'bg-surface-0 dark:bg-surface-900', 14 | 'text-surface-700 dark:text-surface-0/80', 15 | 'ring-1 ring-inset ring-surface-300 dark:ring-surface-700 ring-offset-0' 16 | ] 17 | }, 18 | legend: ({ props }) => ({ 19 | class: [ 20 | // Font 21 | 'font-medium', 22 | 'leading-none', 23 | 24 | //Spacing 25 | { 'p-0': props.toggleable, 'px-3 py-1.5': !props.toggleable }, 26 | 27 | // Shape 28 | 'rounded-md', 29 | 30 | // Color 31 | 'text-surface-700 dark:text-surface-0/80', 32 | 33 | 'bg-surface-0 dark:bg-surface-900', 34 | 35 | // Transition 36 | 'transition-none', 37 | 38 | // States 39 | { '': props.toggleable }, 40 | { 'focus:outline-none focus:outline-offset-0 focus:ring-2 focus:ring-primary-600 ring-inset dark:focus:ring-primary-500': props.toggleable } 41 | ] 42 | }), 43 | toggler: ({ props }) => ({ 44 | class: [ 45 | // Alignments 46 | 'flex items-center justify-center', 47 | 'relative', 48 | 49 | //Spacing 50 | { 'px-3 py-1.5': props.toggleable }, 51 | 52 | // Shape 53 | { 'rounded-md': props.toggleable }, 54 | 55 | // Color 56 | { 'text-surface-700 dark:text-surface-200 hover:text-surface-900 hover:text-surface-900': props.toggleable }, 57 | 58 | // States 59 | { 'hover:text-surface-900 dark:hover:text-surface-100': props.toggleable }, 60 | { 'focus:outline-none focus:outline-offset-0 focus:ring-2 focus:ring-inset focus:ring-primary-600 dark:focus:ring-primary-500': props.toggleable }, 61 | 62 | // Misc 63 | { 64 | 'transition-none cursor-pointer overflow-hidden select-none': props.toggleable 65 | } 66 | ] 67 | }), 68 | togglerIcon: { 69 | class: 'mr-2 inline-block' 70 | }, 71 | legendTitle: { 72 | class: 'flex items-center justify-center leading-none' 73 | }, 74 | content: { 75 | class: 'p-0' 76 | }, 77 | transition: { 78 | enterFromClass: 'max-h-0', 79 | enterActiveClass: 'overflow-hidden transition-[max-height] duration-1000 ease-[cubic-bezier(0.42,0,0.58,1)]', 80 | enterToClass: 'max-h-[1000px]', 81 | leaveFromClass: 'max-h-[1000px]', 82 | leaveActiveClass: 'overflow-hidden transition-[max-height] duration-[450ms] ease-[cubic-bezier(0,1,0,1)]', 83 | leaveToClass: 'max-h-0' 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/test/suite/services/qualifier-service.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { extractUseStatements, getAst, isNamespaced } from '../../../services/laravel/code-runner/qualifier-service'; 3 | 4 | describe('extractUseStatements', () => { 5 | it('should extract plain use statements', () => { 6 | const code = ` 7 | { 19 | const code = ` 20 | { 31 | const code = ` 32 | { 43 | const code = ` 44 | { 57 | it('should return true for code with a namespace', () => { 58 | const code = ` 59 | { 70 | const code = ` 71 | { 81 | const code = ` 82 | ({ 6 | class: [ 7 | // Alignments 8 | 'flex items-center justify-between', 9 | 10 | // Colors 11 | 'text-surface-700 dark:text-surface-0/80', 12 | 'bg-surface-0 dark:bg-surface-900', 13 | 'border-b border-surface-200 dark:border-surface-800', 14 | 15 | //Shape 16 | 'rounded-tl-lg rounded-tr-lg', 17 | 18 | // Conditional Spacing 19 | { 'px-5 md:px-6 py-5': !props.toggleable, 'py-3 px-5 md:px-6': props.toggleable } 20 | ] 21 | }), 22 | title: { 23 | class: 'leading-none font-medium' 24 | }, 25 | toggler: { 26 | class: [ 27 | // Alignments 28 | 'inline-flex items-center justify-center', 29 | 30 | // Sized 31 | 'w-8 h-8', 32 | 33 | //Shape 34 | 'border-0 rounded-full', 35 | 36 | //Color 37 | 'bg-transparent', 38 | 'text-surface-600 dark:text-surface-100/80', 39 | 40 | // States 41 | 'hover:text-surface-900 dark:hover:text-surface-0/80', 42 | 'hover:bg-surface-50 dark:hover:bg-surface-800/50', 43 | 'focus:outline-none focus:outline-offset-0 focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-inset dark:focus-visible:ring-primary-500', 44 | 45 | // Transitions 46 | 'transition duration-200 ease-in-out', 47 | 48 | // Misc 49 | 'overflow-hidden relative no-underline' 50 | ] 51 | }, 52 | togglerIcon: { 53 | class: 'inline-block' 54 | }, 55 | content: { 56 | class: [ 57 | // Spacing 58 | 'py-6 px-5 md:px-6', 59 | 60 | // Shape 61 | 'last:rounded-br-lg last:rounded-bl-lg', 62 | 63 | //Color 64 | 'bg-surface-0 dark:bg-surface-900', 65 | 'text-surface-700 dark:text-surface-0/80' 66 | ] 67 | }, 68 | footer: { 69 | class: [ 70 | // Spacing 71 | 'py-6 px-5 md:px-6', 72 | 73 | //Shape 74 | 'rounded-bl-lg rounded-br-lg', 75 | 76 | // Color 77 | 'bg-surface-0 dark:bg-surface-900', 78 | 'text-surface-600 dark:text-surface-0/70', 79 | 'border-t border-surface-200 dark:border-surface-800' 80 | ] 81 | }, 82 | transition: { 83 | enterFromClass: 'max-h-0', 84 | enterActiveClass: 'overflow-hidden transition-[max-height] duration-1000 ease-[cubic-bezier(0.42,0,0.58,1)]', 85 | enterToClass: 'max-h-[1000px]', 86 | leaveFromClass: 'max-h-[1000px]', 87 | leaveActiveClass: 'overflow-hidden transition-[max-height] duration-[450ms] ease-[cubic-bezier(0,1,0,1)]', 88 | leaveToClass: 'max-h-0' 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/dock/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | // Positioning 5 | 'absolute z-1', 6 | { 7 | 'left-0 bottom-0 w-full': props.position == 'bottom', 8 | 'left-0 top-0 w-full': props.position == 'top', 9 | 'left-0 top-0 h-full': props.position == 'left', 10 | 'right-0 top-0 h-full': props.position == 'right' 11 | }, 12 | 13 | // Flexbox & Alignment 14 | 'flex justify-center items-center', 15 | 16 | // Interactivity 17 | 'pointer-events-none' 18 | ] 19 | }), 20 | container: { 21 | class: [ 22 | // Flexbox 23 | 'flex', 24 | 25 | // Shape & Border 26 | 'rounded-md', 27 | 28 | // Color 29 | 'bg-surface-0/10 dark:bg-surface-900/20 border border-surface-0/20', 30 | 'backdrop-blur-sm', 31 | 32 | // Spacing 33 | 'p-2', 34 | 35 | // Misc 36 | 'pointer-events-auto' 37 | ] 38 | }, 39 | menu: ({ props }) => ({ 40 | class: [ 41 | // Flexbox & Alignment 42 | 'flex items-center justify-center', 43 | { 44 | 'flex-col': props.position == 'left' || props.position == 'right' 45 | }, 46 | 47 | // List Style 48 | 'm-0 p-0 list-none', 49 | 50 | // Shape 51 | 'outline-none' 52 | ] 53 | }), 54 | menuitem: ({ props, context, instance }) => ({ 55 | class: [ 56 | // Spacing & Shape 57 | 'p-2 rounded-md', 58 | 59 | // Conditional Scaling 60 | { 61 | 'hover:scale-150': instance.currentIndex === context.index, 62 | 'scale-125': instance.currentIndex - 1 === context.index || instance.currentIndex + 1 === context.index, 63 | 'scale-110': instance.currentIndex - 2 === context.index || instance.currentIndex + 2 === context.index 64 | }, 65 | 66 | // Positioning & Hover States 67 | { 68 | 'origin-bottom hover:mx-6': props.position == 'bottom', 69 | 'origin-top hover:mx-6': props.position == 'top', 70 | 'origin-left hover:my-6': props.position == 'left', 71 | 'origin-right hover:my-6': props.position == 'right' 72 | }, 73 | 74 | // Transitions & Transform 75 | 'transition-all duration-200 ease-cubic-bezier-will-change-transform transform' 76 | ] 77 | }), 78 | action: { 79 | class: [ 80 | // Flexbox & Alignment 81 | 'flex flex-col items-center justify-center', 82 | 83 | // Position 84 | 'relative', 85 | 86 | // Size 87 | 'w-16 h-16', 88 | 89 | // Misc 90 | 'cursor-default overflow-hidden' 91 | ] 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/chips/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | 'flex', 5 | { 6 | 'opacity-60 select-none pointer-events-none cursor-default': props.disabled 7 | } 8 | ] 9 | }), 10 | container: ({ state }) => ({ 11 | class: [ 12 | // Font 13 | 'font-sans sm:text-sm leading-none', 14 | 15 | // Flex 16 | 'flex items-center flex-wrap gap-1', 17 | 18 | // Spacing 19 | 'm-0 py-1 px-3', 20 | 21 | // Size 22 | 'w-full', 23 | 24 | // Shape 25 | 'list-none', 26 | 'rounded-md', 27 | 28 | // Color 29 | 'text-surface-900 dark:text-surface-0', 30 | 'bg-surface-0 dark:bg-surface-900', 31 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500', 32 | 'shadow-sm', 33 | 34 | // States 35 | { 'ring-1 ring-inset ring-surface-300 dark:ring-surface-700 ring-offset-0': !state.focused, 'ring-2 ring-primary-500 dark:ring-primary-400': state.focused }, 36 | 37 | // Transition 38 | 'transition-colors duration-200', 39 | 40 | // Misc 41 | 'cursor-text overflow-hidden', 42 | 'appearance-none' 43 | ] 44 | }), 45 | 46 | inputtoken: { 47 | class: ['py-0.5 px-0', 'inline-flex flex-auto'] 48 | }, 49 | input: { 50 | class: [ 51 | // Font 52 | 'font-sans sm:text-sm leading-none', 53 | 54 | // Size 55 | 'w-full', 56 | 57 | // Spacing 58 | 'p-0 m-0', 59 | 60 | // Shape 61 | 'appearance-none rounded-none', 62 | 'border-0 outline-none', 63 | 64 | // Color 65 | 'text-surface-700 dark:text-white/80', 66 | 'bg-transparent', 67 | 'placeholder:text-surface-400 dark:placeholder:text-surface-500' 68 | ] 69 | }, 70 | token: { 71 | class: [ 72 | // Flexbox 73 | 'inline-flex items-center', 74 | 75 | // Spacing 76 | 'py-0.5 px-3', 77 | 78 | // Shape 79 | 'rounded-[1.14rem]', 80 | 81 | // Colors 82 | 'text-surface-700 dark:text-white/70', 83 | 'bg-surface-200 dark:bg-surface-700' 84 | ] 85 | }, 86 | label: { 87 | class: 'leading-5' 88 | }, 89 | removeTokenIcon: { 90 | class: [ 91 | // Shape 92 | 'rounded-md leading-6', 93 | 94 | // Spacing 95 | 'ml-2', 96 | 97 | // Size 98 | 'w-4 h-4', 99 | 100 | // Transition 101 | 'transition duration-200 ease-in-out', 102 | 103 | // Misc 104 | 'cursor-pointer' 105 | ] 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/tristatecheckbox/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: ['cursor-pointer inline-flex relative select-none align-bottom', 'w-4 h-4'] 4 | }, 5 | box: ({ props, context }) => ({ 6 | class: [ 7 | // Alignment 8 | 'flex', 9 | 'items-center', 10 | 'justify-center', 11 | 12 | // Size 13 | 'w-4', 14 | 'h-4', 15 | 16 | // Shape 17 | 'rounded', 18 | 'border', 19 | 20 | // Colors 21 | 'text-surface-600', 22 | { 23 | 'border-surface-300 bg-surface-0 dark:border-surface-700 dark:bg-surface-900': !context.active, 24 | 'border-primary-500 bg-primary-500 dark:border-primary-400 dark:bg-primary-400': context.active 25 | }, 26 | 27 | { 28 | 'ring-2 ring-primary-500 dark:ring-primary-400': !props.disabled && context.focused, 29 | 'cursor-default opacity-60': props.disabled 30 | }, 31 | 32 | // States 33 | { 34 | 'peer-focus-visible:ring-2 peer-focus-visible:ring-primary-500 dark:peer-focus-visible:ring-primary-400': !props.disabled, 35 | 'cursor-default opacity-60': props.disabled 36 | }, 37 | 38 | // Transitions 39 | 'transition-colors', 40 | 'duration-200' 41 | ] 42 | }), 43 | input: { 44 | class: [ 45 | 'peer', 46 | 47 | // Size 48 | 'w-full ', 49 | 'h-full', 50 | 51 | // Position 52 | 'absolute', 53 | 'top-0 left-0', 54 | 'z-10', 55 | 56 | // Spacing 57 | 'p-0', 58 | 'm-0', 59 | 60 | // Shape 61 | 'rounded', 62 | 'border', 63 | 64 | // Shape 65 | 'opacity-0', 66 | 'rounded-md', 67 | 'outline-none', 68 | 'border-2 border-surface-300 dark:border-surface-700', 69 | 70 | // Misc 71 | 'appareance-none' 72 | ] 73 | }, 74 | checkicon: { 75 | class: [ 76 | // Font 77 | 'text-normal', 78 | 79 | // Size 80 | 'w-3', 81 | 'h-3', 82 | 83 | // Colors 84 | 'text-white dark:text-surface-900', 85 | 86 | // Transitions 87 | 'transition-all', 88 | 'duration-200' 89 | ] 90 | }, 91 | uncheckicon: { 92 | class: [ 93 | // Font 94 | 'text-normal', 95 | 96 | // Size 97 | 'w-3', 98 | 'h-3', 99 | 100 | // Colors 101 | 'text-white dark:text-surface-900', 102 | 103 | // Transitions 104 | 'transition-all', 105 | 'duration-200' 106 | ] 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/toast/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: ({ props }) => ({ 3 | class: [ 4 | //Size and Shape 5 | 'w-96 rounded-md', 6 | 7 | // Positioning 8 | { '-translate-x-2/4': props.position == 'top-center' || props.position == 'bottom-center' } 9 | ] 10 | }), 11 | container: ({ props }) => ({ 12 | class: [ 13 | 'my-4 rounded-md w-full', 14 | 15 | 'shadow-lg', 16 | 'bg-surface-0 dark:bg-surface-800', 17 | 'ring-1 ring-inset ring-surface-200 dark:ring-surface-700 ring-offset-0', 18 | // Colors 19 | { 20 | 'text-blue-500 dark:text-blue-300': props.message.severity == 'info', 21 | 'text-green-500 dark:text-green-300': props.message.severity == 'success', 22 | 'text-orange-500 dark:text-orange-300': props.message.severity == 'warn', 23 | 'text-red-500 dark:text-red-300': props.message.severity == 'error' 24 | } 25 | ] 26 | }), 27 | content: { 28 | class: 'flex items-start p-4' 29 | }, 30 | icon: { 31 | class: [ 32 | // Sizing and Spacing 33 | 'w-5 h-5', 34 | 'mr-2 shrink-0' 35 | ] 36 | }, 37 | text: { 38 | class: [ 39 | // Font and Text 40 | 'text-sm leading-none', 41 | 'ml-2', 42 | 'flex-1' 43 | ] 44 | }, 45 | summary: { 46 | class: 'font-medium block' 47 | }, 48 | detail: { 49 | class: 'mt-1.5 block text-surface-600 dark:text-surface-0/70' 50 | }, 51 | closebutton: { 52 | class: [ 53 | // Flexbox 54 | 'flex items-center justify-center', 55 | 56 | // Size 57 | 'w-6 h-6', 58 | 59 | // Spacing and Misc 60 | 'ml-auto relative', 61 | 62 | // Shape 63 | 'rounded-full', 64 | 65 | // Colors 66 | 'bg-transparent', 67 | 'text-surface-700 dark:text-surface-0/80', 68 | 69 | // Transitions 70 | 'transition duration-200 ease-in-out', 71 | 72 | // States 73 | 'hover:bg-surface-100 dark:hover:bg-surface-700', 74 | 'outline-none focus:ring-1 focus:ring-inset', 75 | 'focus:ring-primary-500 dark:focus:ring-primary-400', 76 | 77 | // Misc 78 | 'overflow-hidden' 79 | ] 80 | }, 81 | closeicon: { 82 | class: [ 83 | // Sizing and Spacing 84 | 'w-3 h-3', 85 | 'shrink-0' 86 | ] 87 | }, 88 | transition: { 89 | enterFromClass: 'opacity-0 translate-y-2/4', 90 | enterActiveClass: 'transition-[transform,opacity] duration-300', 91 | leaveFromClass: 'max-h-[1000px]', 92 | leaveActiveClass: '!transition-[max-height_.45s_cubic-bezier(0,1,0,1),opacity_.3s,margin-bottom_.3s] overflow-hidden', 93 | leaveToClass: 'max-h-0 opacity-0 mb-0' 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /src/test/suite/services/pagination.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { getPaginationFor } from "../../../services/pagination"; 3 | 4 | describe('Pagination Tests', () => { 5 | it('should handle the first page', () => { 6 | const result = getPaginationFor('test', 1, 100, 10); 7 | assert.strictEqual(result.prevPage, undefined); 8 | assert.strictEqual(result.nextPage, 2); 9 | assert.strictEqual(result.endPage, 10); 10 | assert.strictEqual(result.displayText, 'Showing 1 to 10 of 100 records'); 11 | }); 12 | 13 | it('should handle a middle page', () => { 14 | const result = getPaginationFor('test', 5, 100, 10); 15 | assert.strictEqual(result.prevPage, 4); 16 | assert.strictEqual(result.nextPage, 6); 17 | assert.strictEqual(result.endPage, 10); 18 | assert.strictEqual(result.displayText, 'Showing 41 to 50 of 100 records'); 19 | }); 20 | 21 | it('should handle the last page', () => { 22 | const result = getPaginationFor('test', 10, 100, 10); 23 | assert.strictEqual(result.prevPage, 9); 24 | assert.strictEqual(result.nextPage, undefined); 25 | assert.strictEqual(result.endPage, 10); 26 | assert.strictEqual(result.displayText, 'Showing 91 to 100 of 100 records'); 27 | }); 28 | 29 | it('should handle zero total items', () => { 30 | const result = getPaginationFor('test', 1, 0, 10); 31 | assert.strictEqual(result.prevPage, undefined); 32 | assert.strictEqual(result.nextPage, undefined); 33 | assert.strictEqual(result.endPage, 1); 34 | assert.strictEqual(result.displayText, 'Showing 0 to 0 of 0 records'); 35 | }); 36 | 37 | it('should handle a single item', () => { 38 | const result = getPaginationFor('test', 1, 1, 10); 39 | assert.strictEqual(result.prevPage, undefined); 40 | assert.strictEqual(result.nextPage, undefined); 41 | assert.strictEqual(result.endPage, 1); 42 | assert.strictEqual(result.displayText, 'Showing 1 to 1 of 1 records'); 43 | }); 44 | 45 | it('should handle a single page with less items than per page limit', () => { 46 | const result = getPaginationFor('test', 1, 8, 10); 47 | assert.strictEqual(result.prevPage, undefined); 48 | assert.strictEqual(result.nextPage, undefined); 49 | assert.strictEqual(result.endPage, 1); 50 | assert.strictEqual(result.displayText, 'Showing 1 to 8 of 8 records'); 51 | }); 52 | 53 | it('should handle the second page when total items are exactly one page plus one', () => { 54 | const result = getPaginationFor('test', 2, 11, 10); 55 | assert.strictEqual(result.prevPage, 1); 56 | assert.strictEqual(result.nextPage, undefined); 57 | assert.strictEqual(result.endPage, 2); 58 | assert.strictEqual(result.displayText, 'Showing 11 to 11 of 11 records'); 59 | }); 60 | 61 | it('should handle a large number of total items', () => { 62 | const result = getPaginationFor('test', 50, 1000000, 20); 63 | assert.strictEqual(result.prevPage, 49); 64 | assert.strictEqual(result.nextPage, 51); 65 | assert.strictEqual(result.endPage, 50000); 66 | assert.strictEqual(result.displayText, 'Showing 981 to 1,000 of 1,000,000 records'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/menu/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Sizing and Shape 5 | 'min-w-[12rem]', 6 | 'rounded-md', 7 | // Spacing 8 | 'p-1.5', 9 | // Colors 10 | 'bg-surface-0 dark:bg-surface-700', 11 | 'text-surface-700 dark:text-white/80', 12 | 'ring-1 ring-surface-200 dark:ring-surface-700' 13 | ] 14 | }, 15 | menu: { 16 | class: [ 17 | // Spacings and Shape 18 | 'list-none', 19 | 'm-0', 20 | 'p-0', 21 | 'outline-none' 22 | ] 23 | }, 24 | menuitem: { 25 | class: [ 26 | // Space 27 | 'first:mt-0 mt-1' 28 | ] 29 | }, 30 | content: ({ context }) => ({ 31 | class: [ 32 | //Shape 33 | 'rounded-md', 34 | 35 | // Colors 36 | { 37 | 'text-surface-700 dark:text-surface-0': !context.focused, 38 | 'bg-surface-100 text-primary-500 dark:bg-surface-300/10 dark:text-primary-400': context.focused 39 | }, 40 | 41 | // Transitions 42 | 'transition-shadow', 43 | 'duration-200', 44 | 45 | // States 46 | 'hover:text-primary-600 dark:hover:text-primary-400', 47 | 'hover:bg-surface-100 dark:hover:bg-surface-400/10' 48 | ] 49 | }), 50 | action: { 51 | class: [ 52 | 'relative', 53 | 54 | // Font 55 | 'font-semibold', 56 | 57 | // Flexbox 58 | 'flex', 59 | 'items-center', 60 | 61 | // Spacing 62 | 'py-2', 63 | 'px-3', 64 | 65 | // Misc 66 | 'no-underline', 67 | 'overflow-hidden', 68 | 'cursor-pointer', 69 | 'select-none' 70 | ] 71 | }, 72 | icon: { 73 | class: [ 74 | // Spacing 75 | 'mr-2', 76 | 'leading-6', 77 | 'text-sm' 78 | ] 79 | }, 80 | label: { 81 | class: ['leading-6', 'text-sm'] 82 | }, 83 | submenuheader: { 84 | class: [ 85 | // Font 86 | 'font-semibold', 87 | 'text-xs leading-6', 88 | 89 | // Spacing 90 | 'm-0 ', 91 | 'py-1', 92 | 'px-3', 93 | 94 | // Shape 95 | 'rounded-tl-none', 96 | 'rounded-tr-none', 97 | 98 | // Colors 99 | 'bg-surface-0 dark:bg-surface-700', 100 | 'text-surface-600 dark:text-surface-0/60' 101 | ] 102 | }, 103 | transition: { 104 | enterFromClass: 'opacity-0 scale-y-[0.8]', 105 | enterActiveClass: 'transition-[transform,opacity] duration-[120ms] ease-[cubic-bezier(0,0,0.2,1)]', 106 | leaveActiveClass: 'transition-opacity duration-100 ease-linear', 107 | leaveToClass: 'opacity-0' 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /src/providers/mysql/rails-mysql-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 3 | import { MysqlEngine } from '../../database-engines/mysql-engine'; 4 | import { isRailsProject, getRailsEnv, parseDatabaseYml } from '../../services/rails/rails-core'; 5 | import knex from 'knex'; 6 | import { log } from '../../services/logging-service'; 7 | 8 | export const RailsMysqlProvider: DatabaseEngineProvider = { 9 | name: 'Rails MySQL', 10 | type: 'mysql', 11 | id: 'rails-mysql', 12 | description: 'Rails MySQL with config/database.yml', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isRailsProject()) { 17 | return false; 18 | } 19 | 20 | log('Rails MySQL', 'Checking if Rails MySQL provider can be used in the current workspace...'); 21 | 22 | const railsEnv = getRailsEnv(); 23 | const config = await parseDatabaseYml(railsEnv); 24 | 25 | if (!config) { 26 | log('Rails MySQL', 'Failed to parse database.yml configuration using Rails runner'); 27 | vscode.window.showErrorMessage('Failed to parse Rails database configuration. Please ensure Rails is properly installed and the database.yml file is valid.'); 28 | return false; 29 | } 30 | 31 | if (config.adapter !== 'mysql2') { 32 | log('Rails MySQL', `Database adapter is ${config.adapter}, not mysql2`); 33 | return false; 34 | } 35 | 36 | try { 37 | log('Rails MySQL', 'Creating Knex connection with Rails configuration...'); 38 | const connection = knex({ 39 | client: 'mysql2', 40 | connection: { 41 | host: config.host, 42 | port: config.port, 43 | user: config.username, 44 | password: config.password, 45 | database: config.database, 46 | pool: config.pool ? { min: 0, max: config.pool } : undefined 47 | }, 48 | acquireConnectionTimeout: config.timeout || 60000 49 | }); 50 | 51 | this.engine = new MysqlEngine(connection); 52 | log('Rails MySQL', 'MySQL engine created successfully'); 53 | } catch (error) { 54 | const errorMessage = `MySQL connection error: ${String(error)}`; 55 | vscode.window.showErrorMessage(errorMessage); 56 | log('Rails MySQL', errorMessage); 57 | return false; 58 | } 59 | 60 | try { 61 | const isOkay = await this.engine.isOkay(); 62 | if (!isOkay) { 63 | log('Rails MySQL', 'MySQL connection validation failed'); 64 | vscode.window.showErrorMessage('Failed to connect to MySQL database. Please check your Rails database configuration.'); 65 | return false; 66 | } 67 | log('Rails MySQL', 'MySQL connection validated successfully'); 68 | return true; 69 | } catch (error) { 70 | const errorMessage = `MySQL connection validation error: ${String(error)}`; 71 | log('Rails MySQL', errorMessage); 72 | vscode.window.showErrorMessage('Failed to validate MySQL connection. Please check your database server and configuration.'); 73 | return false; 74 | } 75 | }, 76 | 77 | reconnect(): Promise { 78 | return this.canBeUsedInCurrentWorkspace(); 79 | }, 80 | 81 | async getDatabaseEngine(): Promise { 82 | return this.engine; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/services/adonis/adonis-core.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { log } from '../../services/logging-service'; 4 | import { getBasePath, getWorkspaceFileContent } from "../workspace"; 5 | 6 | /** 7 | * Returns the hostname portion of the APP_URL environment variable. 8 | */ 9 | export async function getHostname(): Promise { 10 | const appUrl = await getEnvFileValue('APP_URL') 11 | if (!appUrl) return 12 | 13 | const appUrlWithoutQuotes = appUrl.replace(/"/g, '') 14 | const appUrlWithoutTrailingSlash = appUrlWithoutQuotes.endsWith('/') 15 | ? appUrlWithoutQuotes.substring(0, appUrlWithoutQuotes.length - 1) 16 | : appUrlWithoutQuotes 17 | 18 | const appUrlWithoutProtocol = appUrlWithoutTrailingSlash.replace(/https?:\/\//, '') 19 | const appUrlWithoutPort = appUrlWithoutProtocol.replace(/:\d+/, '') 20 | 21 | return appUrlWithoutPort 22 | } 23 | 24 | export async function getEnvFileValue(envFileKey: string): Promise { 25 | const envFileContents = getWorkspaceFileContent('.env')?.toString() 26 | if (!envFileContents) return 27 | 28 | const lines = envFileContents.split('\n'); 29 | const appUrlLine = lines.find(line => line.startsWith(`${envFileKey}=`)) 30 | if (!appUrlLine) return 31 | 32 | const appUrl = appUrlLine.substring(appUrlLine.indexOf('=') + 1) 33 | const appUrlWithoutQuotes = appUrl.replace(/"/g, '') 34 | const appUrlWithoutTrailingSlash = appUrlWithoutQuotes.endsWith('/') 35 | ? appUrlWithoutQuotes.substring(0, appUrlWithoutQuotes.length - 1) 36 | : appUrlWithoutQuotes 37 | 38 | const appUrlWithoutProtocol = appUrlWithoutTrailingSlash.replace(/https?:\/\//, '') 39 | const appUrlWithoutPort = appUrlWithoutProtocol.replace(/:\d+/, '') 40 | 41 | return appUrlWithoutPort.trim() 42 | } 43 | 44 | export function isAdonisProject() { 45 | const workspaceRoot = getBasePath(); 46 | if (!workspaceRoot) { 47 | log('Adonis PostgreSQL', 'No workspace root found'); 48 | return false; 49 | } 50 | 51 | // Check if this is an Adonis project by looking for config/database.ts or config/database.js 52 | const databaseTsPath = path.join(workspaceRoot, 'config', 'database.ts'); 53 | const databaseJsPath = path.join(workspaceRoot, 'config', 'database.js'); 54 | 55 | if (!fs.existsSync(databaseTsPath) && !fs.existsSync(databaseJsPath)) { 56 | log('Adonis PostgreSQL', 'No Adonis database config file found'); 57 | return false; 58 | } 59 | 60 | // Check package.json for Adonis dependencies 61 | const packageJsonPath = path.join(workspaceRoot, 'package.json'); 62 | if (!fs.existsSync(packageJsonPath)) { 63 | log('Adonis PostgreSQL', 'No package.json found'); 64 | return false; 65 | } 66 | 67 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 68 | const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; 69 | 70 | const hasAdonisDependency = Object.keys(dependencies).some(dep => 71 | dep === '@adonisjs/core' || dep === '@adonisjs/lucid' || dep.startsWith('@adonisjs/') 72 | ); 73 | 74 | if (!hasAdonisDependency) { 75 | log('Adonis PostgreSQL', 'No Adonis dependencies found in package.json'); 76 | return false; 77 | } 78 | 79 | return true 80 | } -------------------------------------------------------------------------------- /src/providers/mysql/django-mysql-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 3 | import { MysqlEngine } from '../../database-engines/mysql-engine'; 4 | import { isDjangoProject, getDatabaseConfig, getDjangoDbAlias } from '../../services/django/django-core'; 5 | import knex from 'knex'; 6 | import { log } from '../../services/logging-service'; 7 | 8 | export const DjangoMysqlProvider: DatabaseEngineProvider = { 9 | name: 'Django MySQL', 10 | type: 'mysql', 11 | id: 'django-mysql', 12 | description: 'Django MySQL with settings.py', 13 | engine: undefined, 14 | 15 | async canBeUsedInCurrentWorkspace(): Promise { 16 | if (!isDjangoProject()) { 17 | return false; 18 | } 19 | 20 | log('Django MySQL', 'Checking if Django MySQL provider can be used in the current workspace...'); 21 | 22 | const djangoDbAlias = getDjangoDbAlias(); 23 | const config = await getDatabaseConfig(djangoDbAlias); 24 | 25 | if (!config) { 26 | log('Django MySQL', 'Failed to parse Django database configuration using Django shell'); 27 | vscode.window.showErrorMessage('Failed to parse Django database configuration. Please ensure Django is properly installed and the settings.py file is valid.'); 28 | return false; 29 | } 30 | 31 | if (config.adapter !== 'mysql') { 32 | log('Django MySQL', `Database adapter is ${config.adapter}, not mysql`); 33 | return false; 34 | } 35 | 36 | try { 37 | log('Django MySQL', 'Creating Knex connection with Django configuration...'); 38 | const connection = knex({ 39 | client: 'mysql2', 40 | connection: { 41 | host: config.host, 42 | port: config.port, 43 | user: config.username, 44 | password: config.password, 45 | database: config.database, 46 | pool: config.pool ? { min: 0, max: config.pool } : undefined 47 | }, 48 | acquireConnectionTimeout: config.timeout || 60000 49 | }); 50 | 51 | this.engine = new MysqlEngine(connection); 52 | log('Django MySQL', 'MySQL engine created successfully'); 53 | } catch (error) { 54 | const errorMessage = `MySQL connection error: ${String(error)}`; 55 | vscode.window.showErrorMessage(errorMessage); 56 | log('Django MySQL', errorMessage); 57 | return false; 58 | } 59 | 60 | try { 61 | const isOkay = await this.engine.isOkay(); 62 | if (!isOkay) { 63 | log('Django MySQL', 'MySQL connection validation failed'); 64 | vscode.window.showErrorMessage('Failed to connect to MySQL database. Please check your Django database configuration.'); 65 | return false; 66 | } 67 | log('Django MySQL', 'MySQL connection validated successfully'); 68 | return true; 69 | } catch (error) { 70 | const errorMessage = `MySQL connection validation error: ${String(error)}`; 71 | log('Django MySQL', errorMessage); 72 | vscode.window.showErrorMessage('Failed to validate MySQL connection. Please check your database server and configuration.'); 73 | return false; 74 | } 75 | }, 76 | 77 | reconnect(): Promise { 78 | return this.canBeUsedInCurrentWorkspace(); 79 | }, 80 | 81 | async getDatabaseEngine(): Promise { 82 | return this.engine; 83 | } 84 | }; -------------------------------------------------------------------------------- /src/services/html.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as vscode from 'vscode'; 3 | 4 | export const FRONTEND_FOLDER_NAME = 'ui-shell' 5 | 6 | export type VueAssets = { 7 | jsFile: string; 8 | cssFile: string; 9 | }; 10 | 11 | /** 12 | * Gets the html for the webview 13 | */ 14 | export function getWebviewHtml(webview: vscode.Webview, jsFile: string, cssFile: string, _extensionUri: vscode.Uri) { 15 | 16 | // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. 17 | const vueAppScriptUri = webview.asWebviewUri(vscode.Uri.joinPath(_extensionUri, FRONTEND_FOLDER_NAME, 'dist', 'assets', jsFile)); 18 | 19 | // Do the same for the stylesheet. 20 | const styleVueAppUri = webview.asWebviewUri(vscode.Uri.joinPath(_extensionUri, FRONTEND_FOLDER_NAME, 'dist', 'assets', cssFile)); 21 | 22 | // Use nonce to allow specific scripts to be run. 23 | const nonce1 = getNonce(); 24 | const nonce2 = getNonce(); 25 | 26 | /** 27 | * Tailwindcss uses svg loaded from data:image..., at least for checkboxes. 28 | */ 29 | const tailwindcss = 'data:' 30 | 31 | return ` 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | `; 45 | } 46 | 47 | /** 48 | * Gets the compiled Vue assets from the Vue project output folder 49 | */ 50 | export async function getVueAssets(context: vscode.ExtensionContext): Promise { 51 | const allFiles = await vscode.workspace.fs.readDirectory(vscode.Uri.file(context.extensionPath)); 52 | 53 | return new Promise(async (resolve, reject) => { 54 | const uiFolder = allFiles.find((item) => item[0] === FRONTEND_FOLDER_NAME && item[1] === vscode.FileType.Directory); 55 | 56 | if (uiFolder) { 57 | const projectFolder = join(context.extensionPath, FRONTEND_FOLDER_NAME, 'dist', 'assets') 58 | const uiFiles: [string, vscode.FileType][] = await vscode.workspace.fs.readDirectory(vscode.Uri.file(projectFolder)); 59 | const jsFile = uiFiles.find((item) => item[1] === vscode.FileType.File && item[0].endsWith('.js')); 60 | const cssFile = uiFiles.find((item) => item[1] === vscode.FileType.File && item[0].endsWith('.css')); 61 | if (!jsFile || !cssFile) return 62 | 63 | resolve({ 64 | jsFile: jsFile[0], 65 | cssFile: cssFile[0] 66 | }) 67 | } 68 | 69 | reject('Could not find UI assets'); 70 | }) 71 | } 72 | 73 | /** 74 | * Generates a random nonce for webview Content Security Policy 75 | */ 76 | function getNonce() { 77 | let text = ''; 78 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 79 | for (let i = 0; i < 32; i++) { 80 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 81 | } 82 | return text; 83 | } -------------------------------------------------------------------------------- /src/providers/sqlite/rails-sqlite-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DatabaseEngine, DatabaseEngineProvider } from '../../types'; 3 | import { SqliteEngine } from '../../database-engines/sqlite-engine'; 4 | import { isRailsProject, parseDatabaseYml, getRailsEnv } from '../../services/rails/rails-core'; 5 | import { getBasePath } from '../../services/workspace'; 6 | import { log } from '../../services/logging-service'; 7 | import * as path from 'path'; 8 | 9 | export const RailsSqliteProvider: DatabaseEngineProvider = { 10 | name: 'Rails SQLite', 11 | type: 'sqlite', 12 | id: 'rails-sqlite', 13 | description: 'Rails SQLite with config/database.yml', 14 | engine: undefined, 15 | 16 | async canBeUsedInCurrentWorkspace(): Promise { 17 | try { 18 | log('Rails SQLite Provider', 'Checking if Rails SQLite provider can be used'); 19 | 20 | if (!isRailsProject()) { 21 | log('Rails SQLite Provider', 'Not a Rails project'); 22 | return false; 23 | } 24 | 25 | const railsEnv = getRailsEnv(); 26 | log('Rails SQLite Provider', `Using Rails environment: ${railsEnv}`); 27 | 28 | const config = await parseDatabaseYml(railsEnv); 29 | 30 | if (!config) { 31 | log('Rails SQLite Provider', 'Failed to parse database configuration using Rails runner'); 32 | vscode.window.showErrorMessage('Rails SQLite: Could not parse database configuration. Please ensure Rails is properly installed and the database.yml file is valid.'); 33 | return false; 34 | } 35 | 36 | if (config.adapter !== 'sqlite') { 37 | log('Rails SQLite Provider', `Database adapter is ${config.adapter}, not sqlite`); 38 | return false; 39 | } 40 | 41 | if (!config.database) { 42 | log('Rails SQLite Provider', 'No database path specified in configuration'); 43 | vscode.window.showErrorMessage('Rails SQLite: No database path specified in config/database.yml'); 44 | return false; 45 | } 46 | 47 | let databasePath = config.database; 48 | const workspaceRoot = getBasePath(); 49 | 50 | if (workspaceRoot && !path.isAbsolute(databasePath)) { 51 | databasePath = path.resolve(workspaceRoot, databasePath); 52 | log('Rails SQLite Provider', `Resolved relative database path to: ${databasePath}`); 53 | } 54 | 55 | try { 56 | this.engine = new SqliteEngine(databasePath); 57 | } catch (error) { 58 | log('Rails SQLite Provider', `Failed to create SQLite engine: ${String(error)}`); 59 | vscode.window.showErrorMessage(`Rails SQLite file error ${databasePath}: ${String(error)}`); 60 | return false; 61 | } 62 | 63 | const isOkay = await this.engine.isOkay(); 64 | log('Rails SQLite Provider', `Database connection test result: ${isOkay}`); 65 | 66 | if (!isOkay) { 67 | vscode.window.showErrorMessage(`Rails SQLite: Database file is not accessible or corrupted: ${databasePath}`); 68 | } 69 | 70 | return isOkay; 71 | } catch (error) { 72 | log('Rails SQLite Provider', `Unexpected error in canBeUsedInCurrentWorkspace: ${String(error)}`); 73 | vscode.window.showErrorMessage(`Rails SQLite: Unexpected error while checking database configuration: ${String(error)}`); 74 | return false; 75 | } 76 | }, 77 | 78 | reconnect(): Promise { 79 | return this.canBeUsedInCurrentWorkspace(); 80 | }, 81 | 82 | async getDatabaseEngine(): Promise { 83 | return this.engine; 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /ui-shell/src/assets/tailwind-primevue-preset/tieredmenu/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | class: [ 4 | // Shape 5 | 'rounded-md', 6 | 7 | // Size 8 | 'min-w-[12rem]', 9 | 'p-1.5', 10 | 11 | // Colors 12 | 'bg-surface-0 dark:bg-surface-700', 13 | 'ring-1 ring-surface-200 dark:ring-surface-700' 14 | ] 15 | }, 16 | menu: { 17 | class: [ 18 | // Spacings and Shape 19 | 'list-none', 20 | 'm-0', 21 | 'p-0', 22 | 'outline-none' 23 | ] 24 | }, 25 | menuitem: { 26 | class: ['relative first:mt-0 mt-1'] 27 | }, 28 | content: ({ context }) => ({ 29 | class: [ 30 | //Shape 31 | 'rounded-md', 32 | 33 | // Colors 34 | { 35 | 'text-surface-500 dark:text-white/70': !context.focused && !context.active, 36 | 'text-surface-500 dark:text-white/70 bg-surface-200 dark:bg-black/70': context.focused && !context.active, 37 | 'text-surface-900 dark:text-surface-0/80 bg-surface-50 dark:bg-black/70': context.focused && context.active, 38 | 'text-surface-900 dark:text-surface-0/80 bg-surface-50 dark:bg-black/70': !context.focused && context.active 39 | }, 40 | 41 | // Hover States 42 | { 43 | 'hover:bg-surface-50 dark:hover:bg-surface-800': !context.active, 44 | 'hover:bg-surface-100 dark:hover:bg-black/40 text-surface-900 dark:text-surface-0/80': context.active 45 | }, 46 | 47 | // Transitions 48 | 'transition-shadow', 49 | 'duration-200' 50 | ] 51 | }), 52 | action: { 53 | class: [ 54 | 'relative', 55 | 56 | // Font 57 | 'font-semibold', 58 | 59 | // Flexbox 60 | 'flex', 61 | 'items-center', 62 | 63 | // Spacing 64 | 'py-2', 65 | 'px-3', 66 | 67 | // Misc 68 | 'no-underline', 69 | 'overflow-hidden', 70 | 'cursor-pointer', 71 | 'select-none' 72 | ] 73 | }, 74 | icon: { 75 | class: [ 76 | // Spacing 77 | 'mr-2', 78 | 'leading-6', 79 | 'text-sm' 80 | ] 81 | }, 82 | label: { 83 | class: ['leading-none', 'text-sm'] 84 | }, 85 | submenuicon: { 86 | class: [ 87 | // Position 88 | 'ml-auto' 89 | ] 90 | }, 91 | submenu: { 92 | class: [ 93 | // Size 94 | 'w-full sm:w-48', 95 | 96 | // Spacing 97 | 'p-1.5', 98 | 'm-0 mx-1.5', 99 | 'list-none', 100 | 101 | // Shape 102 | 'shadow-none sm:shadow-md', 103 | 'border-0', 104 | 105 | // Position 106 | 'static sm:absolute', 107 | 'z-10', 108 | 109 | // Color 110 | 'bg-surface-0 dark:bg-surface-700' 111 | ] 112 | }, 113 | separator: { 114 | class: 'border-t border-surface-200 dark:border-surface-600 my-1' 115 | } 116 | }; 117 | --------------------------------------------------------------------------------