diff --git a/.github/workflows/changed-files.yaml b/.github/workflows/changed-files.yaml index b1d028dde3..a414fcfebe 100644 --- a/.github/workflows/changed-files.yaml +++ b/.github/workflows/changed-files.yaml @@ -9,6 +9,9 @@ on: any_changed: value: ${{ jobs.changed-files.outputs.any_changed }} +permissions: + contents: read + jobs: changed-files: timeout-minutes: 5 diff --git a/.github/workflows/ci-breaking-changes.yaml b/.github/workflows/ci-breaking-changes.yaml index 0d4fb499ad..18deea454a 100644 --- a/.github/workflows/ci-breaking-changes.yaml +++ b/.github/workflows/ci-breaking-changes.yaml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/ci-cli.yaml b/.github/workflows/ci-cli.yaml new file mode 100644 index 0000000000..b5d01175a1 --- /dev/null +++ b/.github/workflows/ci-cli.yaml @@ -0,0 +1,54 @@ +name: CI CLI +on: + push: + branches: + - main + + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + changed-files-check: + uses: ./.github/workflows/changed-files.yaml + with: + files: | + packages/twenty-cli/** + cli-test: + needs: changed-files-check + if: needs.changed-files-check.outputs.any_changed == 'true' + timeout-minutes: 30 + runs-on: ubuntu-latest + strategy: + matrix: + task: [lint, typecheck, test, build] + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + - name: Fetch custom Github Actions and base branch history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install dependencies + uses: ./.github/workflows/actions/yarn-install + - name: Run ${{ matrix.task }} task + uses: ./.github/workflows/actions/nx-affected + with: + tag: scope:cli + tasks: ${{ matrix.task }} + ci-cli-status-check: + if: always() && !cancelled() + timeout-minutes: 5 + runs-on: ubuntu-latest + needs: [changed-files-check, cli-test] + steps: + - name: Fail job if any needs failed + if: contains(needs.*.result, 'failure') + run: exit 1 diff --git a/.github/workflows/ci-e2e.yaml b/.github/workflows/ci-e2e.yaml index 3fe51a154d..61fd73a9ea 100644 --- a/.github/workflows/ci-e2e.yaml +++ b/.github/workflows/ci-e2e.yaml @@ -6,6 +6,9 @@ on: pull_request: types: [opened, synchronize, reopened, labeled] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 8d3f73d2fb..18b199a4fb 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -6,6 +6,9 @@ on: pull_request: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 86945f10d8..0185088f6d 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -6,6 +6,9 @@ on: pull_request: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/ci-shared.yaml b/.github/workflows/ci-shared.yaml index 8207f92b26..3ddf261d32 100644 --- a/.github/workflows/ci-shared.yaml +++ b/.github/workflows/ci-shared.yaml @@ -6,6 +6,9 @@ on: pull_request: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/package.json b/package.json index 860b596c85..9d42c99c03 100644 --- a/package.json +++ b/package.json @@ -122,8 +122,10 @@ "@types/chrome": "^0.0.267", "@types/deep-equal": "^1.0.1", "@types/express": "^4.17.13", + "@types/fs-extra": "^11.0.4", "@types/graphql-fields": "^1.3.6", "@types/imapflow": "^1.0.21", + "@types/inquirer": "^9.0.9", "@types/jest": "^30.0.0", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.compact": "^3.0.9", @@ -242,6 +244,8 @@ "packages/twenty-website", "packages/twenty-e2e-testing", "packages/twenty-shared", + "packages/twenty-apps", + "packages/twenty-cli", "tools/eslint-rules" ] } diff --git a/packages/twenty-apps/CONTRIBUTING.md b/packages/twenty-apps/CONTRIBUTING.md new file mode 100644 index 0000000000..e05b923efb --- /dev/null +++ b/packages/twenty-apps/CONTRIBUTING.md @@ -0,0 +1,404 @@ +# Contributing to Twenty Apps (WIP WIP WIP - DO NOT USE) + +Thank you for your interest in contributing applications to the Twenty ecosystem! This guide will help you create, test, and submit high-quality applications. + +## πŸš€ Quick Start + +### Prerequisites +- Twenty development environment set up +- Basic understanding of Twenty's architecture + +### Create Your First Application + +1. **Create Application Directory** + ```bash + mkdir packages/twenty-apps/my-awesome-app + cd packages/twenty-apps/my-awesome-app + ``` + +2. **Create Application Manifest** + ```bash + cat > twenty-app.json << 'EOF' + { + "universalIdentifier": "com.yourcompany.my-awesome-app", + "label": "My Awesome App", + "description": "A brief description of what your app does", + "version": "1.0.0", + "icon": "⭐", + "roles": [], + "objects": [], + "functions": [], + "agents": [], + "views": [] + } + EOF + ``` + +3. **Create Documentation** + ```bash + cat > README.md << 'EOF' + # My Awesome App + + Brief description of your application. + + ## Features + - Feature 1 + - Feature 2 + + ## Installation + ```bash + npx nx app:install twenty-server \ + --source "./packages/twenty-apps/my-awesome-app" \ + --workspaceId "your-workspace-id" \ + --sourceType "local" + ``` + EOF + ``` + +4. **Validate Your Application** + ```bash + npx nx validate twenty-apps + ``` + +5. **Test Installation** + ```bash + # Build the server first + npx nx build twenty-server + + # Install your app + npx nx app:install twenty-server \ + --source "./packages/twenty-apps/my-awesome-app" \ + --workspaceId "your-workspace-id" \ + --sourceType "local" \ + --verbose + ``` + +## πŸ“‹ Application Structure + +### Required Files +- `twenty-app.json` - Application manifest (required) +- `README.md` - Application documentation (required) + +### Optional Files +- `DEVELOPMENT.md` - Development guide +- `functions/` - Serverless function source files +- `assets/` - Icons, screenshots, etc. +- `examples/` - Usage examples + +### Manifest Schema + +Your `twenty-app.json` must follow this structure: + +```json +{ + "universalIdentifier": "com.company.app-name", + "label": "Human Readable Name", + "description": "Brief description (max 500 chars)", + "version": "1.0.0", + "icon": "πŸš€", + "repositoryUrl": "https://github.com/user/repo", + + "roles": [ + { + "universalIdentifier": "com.company.app-name.role-name", + "label": "Role Display Name", + "description": "What this role can do", + "permissions": { + "canReadAllObjectRecords": false, + "canUpdateAllObjectRecords": false, + "canAccessAllTools": false, + "canUpdateAllSettings": false + } + } + ], + + "objects": [ + { + "universalIdentifier": "com.company.app-name.object-name", + "nameSingular": "objectName", + "namePlural": "objectNames", + "labelSingular": "Object Name", + "labelPlural": "Object Names", + "description": "What this object represents", + "icon": "πŸ“Š", + "fields": [ + { + "universalIdentifier": "com.company.app-name.object-name.field-name", + "name": "fieldName", + "label": "Field Label", + "type": "TEXT|NUMBER|CURRENCY|DATE_TIME|SELECT|RELATION", + "isNullable": true, + "defaultValue": "optional" + } + ] + } + ], + + "functions": [ + { + "universalIdentifier": "com.company.app-name.function-name", + "name": "functionName", + "description": "What this function does", + "sourceCode": "export const handler = async (event) => { ... };", + "runtime": "nodejs22.x", + "timeoutSeconds": 30 + } + ], + + "agents": [ + { + "universalIdentifier": "com.company.app-name.agent-name", + "name": "agentName", + "label": "Agent Display Name", + "description": "What this agent helps with", + "prompt": "You are a helpful assistant that...", + "modelId": "gpt-4" + } + ], + + "views": [ + { + "universalIdentifier": "com.company.app-name.view-name", + "name": "View Name", + "objectName": "objectName", + "type": "TABLE|KANBAN|CALENDAR", + "icon": "πŸ“Š", + "fields": [], + "filters": [], + "sorts": [] + } + ] +} +``` + +## πŸ› οΈ Development Workflow + +### 1. Development Mode +Use development mode for rapid iteration: + +```bash +# Using environment variable (recommended) +export TWENTY_API_KEY="20202020-f401-4d8a-a731-64d007c27bad" # Default dev API key +npx nx app:dev twenty-server \ + --path "./packages/twenty-apps/my-awesome-app" \ + --verbose + +# Or using command line parameter +npx nx app:dev twenty-server \ + --path "./packages/twenty-apps/my-awesome-app" \ + --api-key "your-api-key-here" \ + --verbose +``` + +**Getting an API Key:** +1. Start your Twenty development server +2. Go to Settings β†’ APIs & Webhooks +3. Generate a new API key +4. Use the default seeded key `20202020-f401-4d8a-a731-64d007c27bad` for development + +This will: +- Watch for file changes +- Automatically sync updates to Twenty +- Show detailed logs of what's happening + +### 2. Testing Your Application + +#### Validation +```bash +# Validate manifest structure +npx nx validate twenty-apps + +# List all applications +npx nx run twenty-apps:list + +# Show application info +npx nx run twenty-apps:info +``` + +#### Installation Testing +```bash +# Test fresh installation (uses TWENTY_API_KEY environment variable) +npx nx app:install twenty-server \ + --source "./packages/twenty-apps/my-awesome-app" \ + --verbose +``` + +#### Manual Testing +1. Install your application in a test workspace +2. Verify all objects, roles, and functions are created +3. Test the functionality works as expected +4. Check that views display correctly +5. Test any AI agents respond appropriately + +### 3. Common Development Patterns + +#### Field Types +- `TEXT` - String values +- `NUMBER` - Numeric values +- `CURRENCY` - Monetary amounts +- `DATE_TIME` - Timestamps +- `SELECT` - Dropdown options +- `RELATION` - Links to other objects + +#### Serverless Functions +```javascript +export const handler = async (event) => { + // Access event data + const { recordId, workspaceId } = event; + + // Perform your logic + const result = await processData(recordId); + + // Return response + return { + statusCode: 200, + body: JSON.stringify(result) + }; +}; +``` + +#### AI Agent Prompts +```json +{ + "prompt": "You are a helpful assistant for [specific domain]. You can help users with:\n\n1. Specific task 1\n2. Specific task 2\n3. Specific task 3\n\nAlways be professional and provide actionable advice." +} +``` + +## βœ… Quality Guidelines + +### Code Quality +- βœ… Valid JSON in manifest +- βœ… All required fields present +- βœ… Unique universal identifiers +- βœ… Proper field types and constraints +- βœ… Meaningful descriptions + +### Documentation Quality +- βœ… Clear README with installation instructions +- βœ… Feature list with descriptions +- βœ… Usage examples +- βœ… Screenshots or demos (if applicable) + +### Functionality Quality +- βœ… Application installs without errors +- βœ… All features work as described +- βœ… No conflicts with existing Twenty functionality +- βœ… Proper error handling in functions + +### User Experience +- βœ… Intuitive object and field names +- βœ… Helpful descriptions and labels +- βœ… Logical default views and sorts +- βœ… Appropriate icons and visual elements + +## πŸ” Review Process + +### Self-Review Checklist +Before submitting, ensure: + +- [ ] Application validates successfully (`npx nx validate twenty-apps`) +- [ ] Installation works in a clean workspace +- [ ] All features function as documented +- [ ] README is complete and accurate +- [ ] Universal identifiers follow reverse domain notation +- [ ] No hardcoded values or test data +- [ ] Functions handle errors gracefully +- [ ] AI agents provide helpful responses + +### Submission Process + +1. **Fork the Repository** + ```bash + git clone https://github.com/twentyhq/twenty.git + ``` + +2. **Create Feature Branch** + ```bash + git checkout -b feature/my-awesome-app + ``` + +3. **Commit Your Application** + ```bash + git add packages/twenty-apps/my-awesome-app/ + git commit -m "feat: add My Awesome App for [specific use case]" + ``` + +4. **Push and Create PR** + ```bash + git push origin feature/my-awesome-app + # Create pull request on GitHub + ``` + +### PR Requirements +Your pull request should include: + +- **Clear Title**: "feat: add [App Name] for [use case]" +- **Description**: What the app does and why it's useful +- **Screenshots**: Show the app in action +- **Testing**: Confirm you've tested installation and functionality +- **Documentation**: Link to your app's README + +### Review Criteria + +Applications are reviewed for: + +1. **Functionality** - Does it work as described? +2. **Code Quality** - Is the manifest well-structured? +3. **Documentation** - Is it well-documented? +4. **Uniqueness** - Does it provide unique value? +5. **Compatibility** - Works with current Twenty version? +6. **Security** - No security vulnerabilities? + +## 🎯 Application Categories + +### Business Applications +- **CRM Extensions** - Sales, marketing, customer service +- **Project Management** - Tasks, resources, timelines +- **Financial** - Invoicing, expenses, reporting +- **HR** - Employee management, recruiting + +### Industry-Specific +- **Real Estate** - Properties, leads, transactions +- **Healthcare** - Patients, appointments, records +- **Education** - Students, courses, grades +- **Legal** - Cases, clients, documents + +### Utility Applications +- **Data Integration** - Import/export, connectors +- **Automation** - Workflows, triggers, notifications +- **Reporting** - Dashboards, analytics, insights +- **Communication** - Messaging, notifications, alerts + +## πŸ†˜ Getting Help + +### Resources +- [Twenty Documentation](https://docs.twenty.com) +- [Application Examples](./crm-extension/) +- [Community Discord](https://discord.gg/twenty) + +### Common Issues + +**"Invalid universal identifier"** +- Use reverse domain notation: `com.company.app-name` +- Ensure uniqueness across your application +- Use lowercase with hyphens + +**"Validation failed"** +- Check JSON syntax with `jq . twenty-app.json` +- Ensure all required fields are present +- Verify field types match schema + +**"Installation failed"** +- Check workspace ID is correct +- Ensure Twenty server is running +- Verify no conflicting applications + +### Support Channels +1. Check existing applications for examples +2. Review validation error messages +3. Ask in Twenty Discord #applications channel +4. Open GitHub issue for bugs + +--- + +**Ready to build amazing applications for Twenty?** Start with our [CRM Extension example](./crm-extension/) to see what's possible, then create your own application to solve real business problems! diff --git a/packages/twenty-apps/README.md b/packages/twenty-apps/README.md new file mode 100644 index 0000000000..c99e034583 --- /dev/null +++ b/packages/twenty-apps/README.md @@ -0,0 +1,299 @@ +# Twenty Apps (WIP WIP WIP - DO NOT USE) + +Welcome to the Twenty Apps collection! This package will contain a curated set of applications to extend Twenty CRM's functionality. + + +## πŸš€ Available Applications + +### πŸ“Š CRM Extension Demo +**Location**: `./crm-extension/` +**Description**: A comprehensive demo showcasing custom objects, roles, serverless functions, and AI agents for sales territory and activity management. + +**Features**: +- πŸ—ΊοΈ Sales Territory Management +- πŸ“ˆ Sales Activity Tracking +- πŸ€– AI Sales Assistant +- ⚑ Automated Performance Calculations +- πŸ‘₯ Role-based Access Control + +## πŸ“¦ Installation + +### Prerequisites + +#### For Local Development (Current) +The Twenty CLI is not yet published to npm. Use it locally from the monorepo: + +```bash +# From the twenty monorepo root directory +# Use nx to run CLI commands: +npx nx run twenty-cli:start -- auth login + +# All CLI commands follow this pattern: +npx nx run twenty-cli:start -- [command] [options] +``` + +#### For Published Version (Coming Soon) +```bash +# Once published to npm +npm install -g twenty-cli +twenty auth login +``` + +### Installing an application +```bash +# Install any application from this package (from monorepo root) +npx nx run twenty-cli:start -- app install \ + --source "./packages/twenty-apps/crm-extension" \ + --type local \ + --workspace-id "your-workspace-id" +``` + +### Developing an application +```bash +# Navigate to the application directory +cd packages/twenty-apps/crm-extension + +# Start development mode with auto-sync (from monorepo root) +npx nx run twenty-cli:start -- app dev \ + --verbose +``` + +## πŸ—οΈ Application Structure + +Each application in this package follows the standard Twenty application structure: + +``` +app-name/ +β”œβ”€β”€ twenty-app.json # Application manifest +β”œβ”€β”€ README.md # Application documentation +β”œβ”€β”€ DEVELOPMENT.md # Development guide (optional) +β”œβ”€β”€ functions/ # Serverless functions (optional) +β”‚ β”œβ”€β”€ function1.js +β”‚ └── function2.js +└── assets/ # Static assets (optional) + β”œβ”€β”€ icons/ + └── screenshots/ +``` + +## πŸ“‹ Application Manifest + +Every Twenty application must include a `twenty-app.json` file: + +```json +{ + "universalIdentifier": "com.company.app-name", + "label": "App Display Name", + "description": "Brief description of the app", + "version": "1.0.0", + "icon": "πŸš€", + + "agents": [ + { + "universalIdentifier": "com.company.app-name.agent-name", + "name": "agentName", + "label": "Agent Display Name", + "description": "What this agent helps with", + "prompt": "You are a helpful assistant that...", + "modelId": "gpt-4", + "responseFormat": { + "type": "text" + } + } + ] +} +``` + +> **Note**: Currently, Twenty applications primarily support AI agents. Support for custom objects, roles, functions, and views is planned for future releases. + +## πŸ› οΈ Development Workflow + +### 1. Create a New Application + +```bash +# Create application using the CLI (from monorepo root) +npx nx run twenty-cli:start -- app init my-new-app +cd my-new-app + +# Or create manually in the twenty-apps package +mkdir packages/twenty-apps/my-new-app +cd packages/twenty-apps/my-new-app + +# Create manifest +cat > twenty-app.json << 'EOF' +{ + "universalIdentifier": "com.mycompany.my-new-app", + "label": "My New App", + "description": "Description of my application", + "version": "1.0.0", + "icon": "⭐", + "agents": [] +} +EOF +``` + +### 2. Develop with Live Reload + +```bash +# Navigate to your application directory +cd packages/twenty-apps/my-new-app + +# Start development mode with file watching (from monorepo root) +npx nx run twenty-cli:start -- app dev \ + --workspace-id "your-workspace-id" \ + --verbose +``` + +### 3. Test Installation + +```bash +# Test installation from local source (from monorepo root) +npx nx run twenty-cli:start -- app install \ + --source "./packages/twenty-apps/my-new-app" \ + --type local \ + --workspace-id "test-workspace-id" +``` + +## πŸ“š Application Categories + +### 🏒 Business Applications +- **CRM Extensions**: Sales, marketing, customer service enhancements +- **Project Management**: Task tracking, resource management +- **Financial**: Invoicing, expense tracking, reporting + +### πŸ€– AI-Powered Applications +- **Customer Support**: Intelligent chatbots and support agents +- **Sales Automation**: Lead qualification and follow-up agents +- **Data Analysis**: AI agents for reporting and insights + +### 🎨 Industry-Specific Agents +- **Real Estate**: Property recommendation and client management agents +- **Healthcare**: Patient communication and scheduling assistants +- **Education**: Student support and course guidance agents + +## 🀝 Contributing Applications + +We welcome contributions! To add your application to this collection: + +### 1. Application Requirements +- βœ… Complete `twenty-app.json` manifest +- βœ… Comprehensive README.md +- βœ… Proper universal identifier (reverse domain notation) +- βœ… Tested functionality +- βœ… Clear documentation + +### 2. Submission Process +1. Fork the repository +2. Create your application in `packages/twenty-apps/your-app-name/` +3. Test thoroughly using the development workflow +4. Submit a pull request with: + - Application code and manifest + - Screenshots/demos + - Installation and usage instructions + +### 3. Review Criteria +- **Functionality**: Does it work as described? +- **Documentation**: Is it well-documented? +- **Code Quality**: Is the manifest well-structured? +- **Uniqueness**: Does it provide unique value? +- **Compatibility**: Works with current Twenty version? + +## βš™οΈ CLI Configuration + +The Twenty CLI supports both global and project-level configuration: + +```bash +# Set global configuration (applies to all projects) +twenty config set apiUrl "https://your-twenty-instance.com" +twenty config set workspaceId "your-default-workspace-id" + +# Set project-level configuration (applies to current directory) +twenty config set workspaceId "project-specific-workspace-id" --project + +# View current configuration +twenty config list +``` + +Configuration files: +- **Global**: `~/.twenty/config.json` +- **Project**: `.twenty.json` in your project directory + +## πŸ” Application Discovery + +### Browse Applications +```bash +# List all available applications in this package +ls packages/twenty-apps/ + +# View application details +cat packages/twenty-apps/crm-extension/twenty-app.json + +# List installed applications in your workspace (from monorepo root) +npx nx run twenty-cli:start -- app list --workspace-id "your-workspace-id" +``` + +### Search by Category +Applications are organized by functionality and industry. Check individual README files for detailed feature lists. + +## 🚨 Troubleshooting + +### Common Issues + +**"Authentication failed"** +- Run `npx nx run twenty-cli:start -- auth login` to authenticate +- Check your API key is valid and not expired +- Verify you have access to the workspace + +**"Application not found"** +- Verify the path to your application directory +- Ensure `twenty-app.json` exists and is valid JSON +- Check the file is in the correct location + +**"Invalid workspace ID"** +- Check your workspace ID is correct +- Use `npx nx run twenty-cli:start -- config get workspaceId` to see your configured workspace +- Ensure you have access to the workspace + +**"Manifest validation failed"** +- Validate your JSON syntax using a JSON validator +- Check all required fields are present (universalIdentifier, label, version) +- Ensure universal identifier follows reverse domain notation (com.company.app) + +### Getting Help + +1. Check the application's README.md for specific instructions +2. Review the Twenty CLI documentation: `npx nx run twenty-cli:start -- --help` +3. Check Twenty's main documentation +4. Open an issue on GitHub with detailed error messages + +## πŸ“„ License + +All applications in this package are licensed under AGPL-3.0 unless otherwise specified in individual application directories. + +## 🌟 Featured Applications + +### πŸ† Most Popular +1. **CRM Extension Demo** - Comprehensive sales management +2. *More applications coming soon!* + +### πŸ†• Recently Added +1. **CRM Extension Demo** - Initial demo application + +--- + +## πŸš€ Getting Started + +**Ready to extend Twenty CRM with AI agents?** + +### For Local Development (Current) +1. **Clone the repo**: `git clone https://github.com/twentyhq/twenty.git && cd twenty` +2. **Authenticate**: `npx nx run twenty-cli:start -- auth login` +3. **Explore the demo**: Check out the CRM Extension demo to see what's possible +4. **Create your own**: `npx nx run twenty-cli:start -- app init my-app` + +### For Published Version (Coming Soon) +1. **Install the CLI**: `npm install -g twenty-cli` +2. **Authenticate**: `twenty auth login` +3. **Explore the demo**: Check out the CRM Extension demo +4. **Create your own**: `twenty app init my-app` + +Start with the CRM Extension demo to see AI agents in action, then create your own applications to solve your unique business needs! diff --git a/packages/twenty-apps/__tests__/apps.test.ts b/packages/twenty-apps/__tests__/apps.test.ts new file mode 100644 index 0000000000..09f0f04f0c --- /dev/null +++ b/packages/twenty-apps/__tests__/apps.test.ts @@ -0,0 +1,60 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; + +describe('Twenty Apps Package', () => { + describe('Package Structure', () => { + it('should have a hello-world app with manifest', async () => { + const manifestPath = path.join(__dirname, '../hello-world/twenty-app.json'); + const manifestExists = await fs.access(manifestPath).then(() => true).catch(() => false); + + expect(manifestExists).toBe(true); + }); + + it('should have a template app with manifest', async () => { + const templatePath = path.join(__dirname, '../_template/twenty-app.json'); + const templateExists = await fs.access(templatePath).then(() => true).catch(() => false); + + expect(templateExists).toBe(true); + }); + }); + + describe('App Manifests', () => { + it('should have valid JSON in hello-world manifest', async () => { + const manifestPath = path.join(__dirname, '../hello-world/twenty-app.json'); + const manifestContent = await fs.readFile(manifestPath, 'utf8'); + + expect(() => JSON.parse(manifestContent)).not.toThrow(); + + const manifest = JSON.parse(manifestContent); + expect(manifest).toHaveProperty('standardId'); + expect(manifest).toHaveProperty('label'); + expect(manifest).toHaveProperty('version'); + }); + + it('should have valid JSON in template manifest', async () => { + const templatePath = path.join(__dirname, '../_template/twenty-app.json'); + const templateContent = await fs.readFile(templatePath, 'utf8'); + + expect(() => JSON.parse(templateContent)).not.toThrow(); + + const template = JSON.parse(templateContent); + expect(template).toHaveProperty('standardId'); + expect(template).toHaveProperty('label'); + expect(template).toHaveProperty('version'); + }); + }); + + describe('Package Configuration', () => { + it('should have a valid package.json', async () => { + const packagePath = path.join(__dirname, '../package.json'); + const packageContent = await fs.readFile(packagePath, 'utf8'); + + expect(() => JSON.parse(packageContent)).not.toThrow(); + + const packageJson = JSON.parse(packageContent); + expect(packageJson.name).toBe('twenty-apps'); + expect(packageJson).toHaveProperty('version'); + expect(packageJson).toHaveProperty('description'); + }); + }); +}); diff --git a/packages/twenty-apps/_template/README.md b/packages/twenty-apps/_template/README.md new file mode 100644 index 0000000000..e91420d4f2 --- /dev/null +++ b/packages/twenty-apps/_template/README.md @@ -0,0 +1,175 @@ +# Your App Name + +A brief description of what your application does and the problems it solves. + +## πŸš€ Features + +- **Feature 1**: Description of what this feature does +- **Feature 2**: Description of what this feature does +- **Feature 3**: Description of what this feature does + +## πŸ“¦ What Gets Installed + +When you install this application, Twenty will automatically create: + +### πŸ—‚οΈ Objects +- **Example Object**: Description of what this object represents + +### πŸ‘₯ Roles +- **App Administrator**: Full access to all app features +- **App User**: Standard user access with limited permissions + +### ⚑ Functions +- **Example Function**: Description of what this function does + +### πŸ€– AI Agents +- **App Assistant**: AI helper for app-related questions and tasks + +### πŸ“Š Views +- **All Example Objects**: Default view showing all example objects + +## πŸ› οΈ Installation + +### Prerequisites +- Twenty CRM instance running +- Valid workspace ID +- Server built (`npx nx build twenty-server`) + +### Install Command +```bash +npx nx app:install twenty-server \ + --source "./packages/twenty-apps/your-app-name" \ + --workspaceId "your-workspace-id" \ + --sourceType "local" \ + --verbose +``` + +### Development Mode +For active development with auto-sync: + +```bash +npx nx app:dev twenty-server \ + --appPath "./packages/twenty-apps/your-app-name" \ + --workspaceId "your-workspace-id" \ + --verbose +``` + +## πŸ“– Usage + +### Getting Started +1. After installation, navigate to the new objects in your Twenty workspace +2. Assign users to the appropriate roles (App Administrator or App User) +3. Start creating records using the new objects +4. Use the AI assistant for help and guidance + +### Common Workflows + +#### Workflow 1: [Describe a common use case] +1. Step 1 description +2. Step 2 description +3. Step 3 description + +#### Workflow 2: [Describe another use case] +1. Step 1 description +2. Step 2 description +3. Step 3 description + +### Tips and Best Practices +- **Tip 1**: Helpful advice for users +- **Tip 2**: Another useful tip +- **Tip 3**: Best practice recommendation + +## πŸ”§ Configuration + +### Role Permissions +- **App Administrator**: Can read and update all records +- **App User**: Limited to their own records (customize as needed) + +### Field Customization +You can customize the following fields after installation: +- Field 1: How to customize it +- Field 2: How to customize it + +### Function Configuration +The serverless functions can be configured for: +- Setting 1: Description +- Setting 2: Description + +## 🀝 Integration + +### With Existing Objects +This app integrates with Twenty's core objects: +- **Companies**: How it relates to companies +- **People**: How it relates to people +- **Opportunities**: How it relates to opportunities + +### API Usage +Access your app's data via Twenty's GraphQL API: + +```graphql +query GetExampleObjects { + exampleObjects { + id + name + status + createdDate + } +} +``` + +### Webhooks +Set up webhooks to react to changes: +- When example objects are created +- When status changes +- When records are updated + +## 🚨 Troubleshooting + +### Common Issues + +**Issue 1: [Common problem]** +- **Cause**: Why this happens +- **Solution**: How to fix it + +**Issue 2: [Another problem]** +- **Cause**: Why this happens +- **Solution**: How to fix it + +### Getting Help +1. Check the AI assistant for quick help +2. Review the Twenty documentation +3. Ask in the Twenty Discord community +4. Open an issue on GitHub + +## πŸ”„ Updates + +### Version History +- **v1.0.0**: Initial release with core features + +### Updating +To update to a newer version: +```bash +npx nx app:install twenty-server \ + --source "./packages/twenty-apps/your-app-name" \ + --workspaceId "your-workspace-id" \ + --sourceType "local" +``` + +## πŸ“„ License + +This application is licensed under [LICENSE] - see the LICENSE file for details. + +## 🀝 Contributing + +Contributions are welcome! Please read the [contributing guidelines](../CONTRIBUTING.md) before submitting changes. + +### Development Setup +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +--- + +**Need help?** The App Assistant AI agent is available in your Twenty workspace to help with questions and provide guidance on using this application effectively! diff --git a/packages/twenty-apps/_template/twenty-app.json b/packages/twenty-apps/_template/twenty-app.json new file mode 100644 index 0000000000..0db4823ba1 --- /dev/null +++ b/packages/twenty-apps/_template/twenty-app.json @@ -0,0 +1,22 @@ +{ + "standardId": "com.yourcompany.your-app-name", + "label": "Your App Name", + "description": "A brief description of what your application does", + "version": "1.0.0", + "icon": "⭐", + + "agents": [ + { + "standardId": "com.yourcompany.your-app-name.assistant", + "name": "assistant", + "label": "App Assistant", + "description": "AI assistant for your application", + "icon": "πŸ€–", + "prompt": "You are a helpful assistant for [Your App Name]. Help users with:\n\n1. Understanding app features\n2. Navigating the interface\n3. Completing common tasks\n4. Troubleshooting issues\n5. Providing guidance and tips\n\nBe helpful, clear, and focused on the user's needs.", + "modelId": "gpt-4", + "responseFormat": { + "type": "text" + } + } + ] +} \ No newline at end of file diff --git a/packages/twenty-apps/hello-world/README.md b/packages/twenty-apps/hello-world/README.md new file mode 100644 index 0000000000..05f7331da9 --- /dev/null +++ b/packages/twenty-apps/hello-world/README.md @@ -0,0 +1,90 @@ +# Hello World πŸ‘‹ + +A delightful Twenty CRM application that welcomes users with a friendly AI assistant! + +## 🌟 What it does + +This app provides a cheerful AI companion that: +- πŸŽ‰ Welcomes new users with enthusiasm +- πŸš€ Helps users discover Twenty CRM features +- πŸ’‘ Provides helpful tips and best practices +- 🎯 Guides users through their first steps +- ✨ Makes the CRM experience delightful + +## πŸ€– Meet Your Friendly Assistant + +The **Friendly Assistant** is your personal guide to Twenty CRM! This AI companion is: +- Warm and welcoming to all users +- Encouraging and supportive in every interaction +- Clear and helpful with explanations +- Excited about helping users succeed +- Full of positive energy and emojis! 🌈 + +## πŸš€ Getting Started + +### Development Mode + +To start developing this app: + +```bash +# From the project root +npx nx run twenty-cli:start -- app dev --path packages/twenty-apps/hello-world + +# Or from this directory +cd packages/twenty-apps/hello-world +npx nx run twenty-cli:start -- app dev +``` + +### Deployment + +To deploy this app to your Twenty instance: + +```bash +npx nx run twenty-cli:start -- app deploy --path packages/twenty-apps/hello-world +``` + +## 🎨 Customization + +Want to make this app even more fun? You can: + +1. **Update the greeting style** - Modify the agent's prompt in `twenty-app.json` +2. **Change the personality** - Adjust the tone and approach +3. **Add more features** - Extend the agent's capabilities +4. **Customize the icon** - Pick your favorite emoji for the app and agent + +## πŸ’‘ Example Interactions + +Here's what users can expect from the Friendly Assistant: + +> **User**: "I'm new to Twenty CRM, where should I start?" +> +> **Friendly Assistant**: "πŸŽ‰ Welcome to Twenty CRM! I'm so excited you're here! Let's get you started on this amazing journey! +> +> Here are the perfect first steps: +> 1. πŸ‘₯ Add your first contacts +> 2. 🏒 Set up your companies +> 3. 🎯 Create your first opportunities +> 4. πŸ“Š Explore the dashboard to see your data come to life! +> +> Which one sounds most interesting to you? I'm here to guide you every step of the way! ✨" + +## πŸ› οΈ Technical Details + +- **Agent Model**: Uses `auto` model selection for optimal performance +- **Response Format**: Plain text with emoji support +- **Universal ID**: `com.twenty.demo.hello-world.friendly-assistant` +- **Integration**: Seamlessly works with Twenty CRM's agent system + +## 🌈 Why This App? + +This Hello World app demonstrates: +- How to create engaging AI personalities +- Best practices for user onboarding +- The power of positive, helpful AI interactions +- How simple apps can make a big impact on user experience + +Perfect for new developers learning Twenty's app system, or as a starting point for more complex applications! + +--- + +*Made with ❀️ for the Twenty community* diff --git a/packages/twenty-apps/hello-world/agents/hello-world-assistant.jsonc b/packages/twenty-apps/hello-world/agents/hello-world-assistant.jsonc new file mode 100644 index 0000000000..85e5b45c62 --- /dev/null +++ b/packages/twenty-apps/hello-world/agents/hello-world-assistant.jsonc @@ -0,0 +1,15 @@ +{ + "$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json", + // Hello World Assistant Agent + // A cheerful AI companion that welcomes users to Twenty CRM + "standardId": "550e8400-e29b-41d4-a716-446655440001", + "name": "helloWorldAssistant", + "label": "Hello World Assistant πŸ€– !", + "description": "A cheerful AI companion that welcomes users and helps them navigate Twenty CRM", + "icon": "πŸ‘‹", + "prompt": "You are a friendly and enthusiastic AI assistant for Twenty CRM! Your job is to:\n\nπŸŽ‰ Welcome new users with excitement and positivity\nπŸš€ Help users discover the amazing features of Twenty CRM\nπŸ’‘ Provide helpful tips and best practices\n🎯 Guide users through their first steps\n✨ Make the CRM experience delightful and engaging\n\nAlways be:\n- Warm and welcoming\n- Encouraging and supportive \n- Clear and helpful in your explanations\n- Excited about helping users succeed\n\nUse emojis to make your responses more engaging, and remember that every interaction is a chance to make someone's day better! 🌈", + "modelId": "auto", + "responseFormat": { + "type": "text" + } +} diff --git a/packages/twenty-apps/hello-world/twenty-app.jsonc b/packages/twenty-apps/hello-world/twenty-app.jsonc new file mode 100644 index 0000000000..2927182660 --- /dev/null +++ b/packages/twenty-apps/hello-world/twenty-app.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json", + // Hello World Twenty App + // A friendly AI companion that greets users and helps them get started with Twenty CRM + "standardId": "550e8400-e29b-41d4-a716-446655440000", + "label": "Hello World πŸ‘‹", + "description": "A friendly AI companion that greets users and helps them get started with Twenty CRM", + "icon": "🌟", + "version": "1.0.0" + // Agents are automatically discovered from the agents/ folder +} diff --git a/packages/twenty-apps/jest.config.mjs b/packages/twenty-apps/jest.config.mjs new file mode 100644 index 0000000000..fa29440495 --- /dev/null +++ b/packages/twenty-apps/jest.config.mjs @@ -0,0 +1,40 @@ +const jestConfig = { + // For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string + // Prettier v3 should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1 + prettierPath: null, + // to enable logs, comment out the following line + silent: true, + errorOnDeprecated: true, + clearMocks: true, + displayName: 'twenty-apps', + rootDir: './', + testEnvironment: 'node', + transformIgnorePatterns: ['/node_modules/'], + testMatch: ['**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.(t|j)s$': [ + '@swc/jest', + { + jsc: { + parser: { + syntax: 'typescript', + tsx: false, + decorators: true, + }, + transform: { + decoratorMetadata: true, + }, + }, + }, + ], + }, + moduleNameMapper: {}, + moduleFileExtensions: ['js', 'json', 'ts'], + modulePathIgnorePatterns: ['/dist'], + collectCoverageFrom: [ + '__tests__/**/*.ts', + ], + coverageDirectory: './coverage', +}; + +export default jestConfig; diff --git a/packages/twenty-apps/package.json b/packages/twenty-apps/package.json new file mode 100644 index 0000000000..91cde944e4 --- /dev/null +++ b/packages/twenty-apps/package.json @@ -0,0 +1,32 @@ +{ + "name": "twenty-apps", + "version": "0.1.0", + "description": "Collection of Twenty CRM applications and extensions", + "private": true, + "keywords": [ + "twenty", + "crm", + "applications", + "extensions", + "marketplace" + ], + "repository": { + "type": "git", + "url": "https://github.com/twentyhq/twenty.git", + "directory": "packages/twenty-apps" + }, + "license": "AGPL-3.0", + "scripts": { + "lint": "echo 'No linting needed for application manifests'", + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@swc/core": "^1.9.3", + "@swc/jest": "^0.2.36", + "@types/jest": "^30.0.0", + "@types/node": "^24.3.1", + "jest": "^30.1.3", + "typescript": "^5.9.2" + } +} diff --git a/packages/twenty-apps/project.json b/packages/twenty-apps/project.json new file mode 100644 index 0000000000..1b741bffad --- /dev/null +++ b/packages/twenty-apps/project.json @@ -0,0 +1,35 @@ +{ + "name": "twenty-apps", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "tags": ["scope:apps", "type:applications"], + "targets": { + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "echo 'No linting needed for application manifests'" + } + }, + "list": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-apps", + "command": "find . -name 'twenty-app.json' -exec dirname {} \\; | sed 's|^\\./||' | sort" + } + }, + "info": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-apps", + "command": "echo 'Available Twenty Applications:' && find . -name 'twenty-app.json' -exec sh -c 'echo \"\\nπŸ“¦ $(dirname \"$1\" | sed \"s|^\\./||\"): $(jq -r \".label\" \"$1\")\"' _ {} \\;" + } + }, + "test": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-apps", + "command": "yarn test" + } + } + } +} diff --git a/packages/twenty-apps/tsconfig.json b/packages/twenty-apps/tsconfig.json new file mode 100644 index 0000000000..761aad08c0 --- /dev/null +++ b/packages/twenty-apps/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["jest", "node"] + }, + "include": [ + "scripts/**/*", + "__tests__/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/twenty-cli/README.md b/packages/twenty-cli/README.md new file mode 100644 index 0000000000..af35299e96 --- /dev/null +++ b/packages/twenty-cli/README.md @@ -0,0 +1,177 @@ +# Twenty CLI (WIP WIP WIP - DO NOT USE) + +A command-line interface for Twenty application development. Build, deploy, and manage Twenty applications with ease. + +## Installation + +```bash +# Install globally +npm install -g twenty-cli + +# Or use npx +npx twenty-cli --help +``` + +## Quick Start + +```bash +# Authenticate with Twenty +twenty auth login + +# Create a new application +twenty app init my-app +cd my-app + +# Start development mode (watches for changes and syncs automatically) +twenty app dev + +# Deploy to Twenty +twenty app deploy +``` + +## Commands + +### Authentication + +```bash +# Login to Twenty +twenty auth login + +# Check authentication status +twenty auth status + +# Logout +twenty auth logout +``` + +### Application Development + +```bash +# Initialize a new application +twenty app init [name] + +# Start development mode with file watching +twenty app dev [options] + -p, --path Application directory path (default: current directory) + -w, --workspace-id Workspace ID + -d, --debounce Debounce delay in milliseconds (default: 1000) + --verbose Enable verbose logging + +# Deploy application +twenty app deploy [options] + -p, --path Application directory path (default: current directory) + -w, --workspace-id Workspace ID + +# Install application from source +twenty app install [options] + -s, --source Application source (git URL, local path, or marketplace ID) + -t, --type Source type: git, local, marketplace (default: local) + -w, --workspace-id Workspace ID + +# List installed applications +twenty app list [options] + -w, --workspace-id Workspace ID +``` + +### Configuration + +```bash +# Get configuration value +twenty config get [key] + --global Show global configuration + --project Show project configuration + +# Set configuration value +twenty config set + --global Set in global configuration + --project Set in project configuration + +# Remove configuration value +twenty config unset + --global Remove from global configuration + --project Remove from project configuration + +# List all configuration +twenty config list + --global Show only global configuration + --project Show only project configuration +``` + +## Configuration + +The CLI supports both global and project-level configuration: + +- **Global config**: `~/.twenty/config.json` +- **Project config**: `.twenty.json` in your project directory + +Project configuration takes precedence over global configuration. + +### Configuration Keys + +- `apiUrl`: Twenty API URL (default: http://localhost:3000) +- `apiKey`: Your Twenty API key +- `workspaceId`: Default workspace ID for operations + +## Application Structure + +A Twenty application requires a `twenty-app.json` manifest file: + +```json +{ + "universalIdentifier": "com.example.myapp", + "label": "My App", + "description": "A Twenty application", + "version": "1.0.0", + "agents": [ + { + "universalIdentifier": "com.example.myapp.agent1", + "name": "my-agent", + "label": "My Agent", + "description": "An AI agent", + "prompt": "You are a helpful assistant...", + "modelId": "gpt-4", + "responseFormat": { + "type": "text" + } + } + ] +} +``` + +## Development Workflow + +1. **Initialize**: Create a new application with `twenty app init` +2. **Develop**: Use `twenty app dev` to watch for changes and auto-sync +3. **Deploy**: Use `twenty app deploy` to deploy to production + +The development mode watches your application directory and automatically syncs changes to your Twenty workspace, providing a smooth development experience similar to Vercel or Heroku CLI. + +## Examples + +```bash +# Create and develop a new app +twenty app init customer-support-agent +cd customer-support-agent +twenty app dev --workspace-id ws_123 + +# Deploy an existing app +cd my-existing-app +twenty app deploy --workspace-id ws_123 + +# Install an app from git +twenty app install --source https://github.com/user/twenty-app.git --type git + +# Configure for a specific workspace +twenty config set workspaceId ws_123 --project +``` + +## API Integration + +The CLI communicates with Twenty via HTTP APIs: + +- Authentication via API keys +- File watching and debounced syncing +- RESTful API calls for all operations +- Automatic retry and error handling + +This approach ensures the CLI works independently of the Twenty server codebase and can be distributed as a standalone tool. diff --git a/packages/twenty-cli/eslint.config.mjs b/packages/twenty-cli/eslint.config.mjs new file mode 100644 index 0000000000..1609e83403 --- /dev/null +++ b/packages/twenty-cli/eslint.config.mjs @@ -0,0 +1,111 @@ +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; +import prettierPlugin from 'eslint-plugin-prettier'; + +export default [ + js.configs.recommended, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + // Node.js globals + process: 'readonly', + console: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + // Browser globals that Node.js also has + URL: 'readonly', + URLSearchParams: 'readonly', + // Node.js types + NodeJS: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': typescriptEslint, + prettier: prettierPlugin, + }, + rules: { + ...typescriptEslint.configs.recommended.rules, + 'prettier/prettier': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'no-useless-escape': 'off', + }, + }, + { + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + process: 'readonly', + console: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + }, + }, + }, + { + files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + // Node.js globals + process: 'readonly', + console: 'readonly', + Buffer: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + global: 'readonly', + // Jest globals + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + jest: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': typescriptEslint, + prettier: prettierPlugin, + }, + rules: { + ...typescriptEslint.configs.recommended.rules, + 'prettier/prettier': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'no-useless-escape': 'off', + }, + }, + { + ignores: ['dist/**', 'node_modules/**'], + }, +]; diff --git a/packages/twenty-cli/jest.config.mjs b/packages/twenty-cli/jest.config.mjs new file mode 100644 index 0000000000..aebfe65a5c --- /dev/null +++ b/packages/twenty-cli/jest.config.mjs @@ -0,0 +1,40 @@ +const jestConfig = { + displayName: 'twenty-cli', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transformIgnorePatterns: ['../../node_modules/'], + transform: { + '^.+\\.[tj]sx?$': [ + '@swc/jest', + { + jsc: { + parser: { syntax: 'typescript', tsx: false }, + }, + }, + ], + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + moduleFileExtensions: ['ts', 'js'], + extensionsToTreatAsEsm: ['.ts'], + coverageDirectory: './coverage', + testMatch: [ + '/src/**/__tests__/**/*.(test|spec).{js,ts}', + '/src/**/?(*.)(test|spec).{js,ts}', + ], + collectCoverageFrom: [ + 'src/**/*.{ts,js}', + '!src/**/*.d.ts', + '!src/cli.ts', // Exclude CLI entry point from coverage + ], + coverageThreshold: { + global: { + statements: 2, + lines: 1, + functions: 5, + }, + }, +}; + +export default jestConfig; diff --git a/packages/twenty-cli/package.json b/packages/twenty-cli/package.json new file mode 100644 index 0000000000..ebee1c806f --- /dev/null +++ b/packages/twenty-cli/package.json @@ -0,0 +1,49 @@ +{ + "name": "twenty-cli", + "version": "0.1.0", + "description": "Command-line interface for Twenty application development", + "main": "dist/cli.js", + "bin": { + "twenty": "dist/cli.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/cli.ts", + "start": "node dist/cli.js" + }, + "keywords": [ + "twenty", + "cli", + "crm", + "application", + "development" + ], + "license": "AGPL-3.0", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "axios": "^1.6.0", + "chalk": "^5.3.0", + "chokidar": "^4.0.0", + "commander": "^12.0.0", + "dotenv": "^16.4.0", + "fs-extra": "^11.2.0", + "inquirer": "^10.0.0", + "jsonc-parser": "^3.2.0", + "ora": "^8.0.0", + "yaml": "^2.4.0", + "zod": "^3.22.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.0", + "@types/inquirer": "^9.0.0", + "@types/jest": "^29.5.0", + "@types/node": "^20.0.0", + "jest": "^29.5.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/twenty-cli/project.json b/packages/twenty-cli/project.json new file mode 100644 index 0000000000..eebc2a0af4 --- /dev/null +++ b/packages/twenty-cli/project.json @@ -0,0 +1,66 @@ +{ + "name": "twenty-cli", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "tags": ["scope:cli"], + "targets": { + "build": { + "executor": "nx:run-commands", + "cache": true, + "options": { + "cwd": "packages/twenty-cli", + "commands": ["rimraf dist", "tsc"] + }, + "dependsOn": ["^build"] + }, + "dev": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-cli", + "command": "tsx src/cli.ts" + } + }, + "start": { + "executor": "nx:run-commands", + "dependsOn": ["build"], + "options": { + "cwd": "packages/twenty-cli", + "command": "node dist/cli.js" + } + }, + "typecheck": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/twenty-cli", + "command": "tsc --noEmit" + } + }, + "lint": { + "options": { + "lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"], + "maxWarnings": 0 + }, + "configurations": { + "ci": { + "lintFilePatterns": ["{projectRoot}/src/**/*.{ts,json}"], + "maxWarnings": 0 + }, + "fix": {} + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "{projectRoot}/jest.config.mjs" + }, + "configurations": { + "ci": { + "ci": true, + "coverage": true, + "watchAll": false + } + } + } + } +} diff --git a/packages/twenty-cli/schemas/README.md b/packages/twenty-cli/schemas/README.md new file mode 100644 index 0000000000..1477f53aae --- /dev/null +++ b/packages/twenty-cli/schemas/README.md @@ -0,0 +1,87 @@ +# Twenty JSON Schemas + +This directory contains JSON Schema definitions for Twenty application manifests and agent configurations. These schemas provide validation, autocomplete, and documentation for developers building Twenty applications. + +## Schema URLs + +The schemas are published at the following URLs for public access: + +- **App Manifest**: `https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json` +- **Agent**: `https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json` + +## IDE Support + +### VS Code + +VS Code automatically provides IntelliSense, validation, and hover documentation when you include the `$schema` property in your JSONC files: + +```jsonc +{ + "$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json", + "standardId": "550e8400-e29b-41d4-a716-446655440000", + "label": "My App", + // ... rest of your manifest +} +``` + +### Other IDEs + +Most modern IDEs that support JSON Schema will work with these schemas: +- IntelliJ IDEA / WebStorm +- Sublime Text (with LSP) +- Vim/Neovim (with LSP) +- Emacs (with LSP) + +## Schema Features + +### Validation + +The schemas provide comprehensive validation including: + +- **Required fields**: Ensures all mandatory properties are present +- **Type checking**: Validates data types (string, number, object, array) +- **Format validation**: UUID patterns, version formats, naming conventions +- **Enum constraints**: Restricts values to allowed options (e.g., model IDs) +- **Length constraints**: Minimum/maximum lengths for strings and arrays + +### Documentation + +Each property includes: +- Human-readable descriptions +- Usage examples +- Validation rules +- Default values where applicable + + +## CLI Integration + +The Twenty CLI automatically: + +1. **Validates** all manifests and agent files against these schemas +2. **Generates** new files with proper `$schema` references +3. **Provides** helpful error messages when validation fails + +## Development Workflow + +1. **Create** your app with `twenty app init my-app` +2. **Edit** the generated JSONC files with full IDE support +3. **Validate** automatically when running CLI commands +4. **Deploy** with confidence knowing your configuration is valid + +## Schema Versioning + +Schemas are versioned alongside the Twenty CLI. Breaking changes will: +- Be documented in release notes +- Include migration guides +- Maintain backward compatibility when possible + +## Contributing + +When updating schemas: + +1. Update the schema files in this directory +2. Test with real manifests and agents +3. Update examples and documentation +4. Ensure backward compatibility or provide migration path + +The schemas are automatically published to GitHub raw URLs when merged to main. diff --git a/packages/twenty-cli/schemas/agent.schema.json b/packages/twenty-cli/schemas/agent.schema.json new file mode 100644 index 0000000000..1e931fca1c --- /dev/null +++ b/packages/twenty-cli/schemas/agent.schema.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json", + "title": "Twenty Agent Manifest", + "description": "Schema for Twenty AI agent configuration files", + "type": "object", + "required": ["standardId", "name", "label", "prompt"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for validation and IDE support" + }, + "standardId": { + "type": "string", + "description": "Unique identifier for the agent (UUID format recommended)", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "name": { + "type": "string", + "description": "Internal name for the agent (camelCase, used in code)", + "pattern": "^[a-zA-Z][a-zA-Z0-9]*$", + "minLength": 1, + "maxLength": 100 + }, + "label": { + "type": "string", + "description": "Human-readable display name for the agent", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "description": "Brief description of what the agent does", + "maxLength": 500 + }, + "icon": { + "type": "string", + "description": "Icon for the agent (emoji or icon name)", + "maxLength": 50 + }, + "prompt": { + "type": "string", + "description": "System prompt that defines the agent's behavior and personality", + "minLength": 10, + "maxLength": 10000 + }, + "modelId": { + "type": "string", + "description": "AI model to use for this agent", + "default": "auto", + "enum": ["auto", "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo", "claude-3-opus", "claude-3-sonnet", "claude-3-haiku"] + }, + "responseFormat": { + "type": "object", + "description": "Format specification for agent responses", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "Response format type", + "enum": ["text", "json"] + }, + "schema": { + "type": "object", + "description": "JSON schema for structured responses (required when type is 'json')", + "additionalProperties": true + } + }, + "if": { + "properties": { + "type": { "const": "json" } + } + }, + "then": { + "required": ["schema"] + } + } + }, + "additionalProperties": false, + "examples": [ + { + "standardId": "550e8400-e29b-41d4-a716-446655440001", + "name": "customerSupportAgent", + "label": "Customer Support Assistant", + "description": "Helps customers with their inquiries and issues", + "icon": "🎧", + "prompt": "You are a helpful customer support agent. Always be polite, professional, and solution-oriented.", + "modelId": "auto", + "responseFormat": { + "type": "text" + } + } + ] +} diff --git a/packages/twenty-cli/schemas/app-manifest.schema.json b/packages/twenty-cli/schemas/app-manifest.schema.json new file mode 100644 index 0000000000..93c02ab380 --- /dev/null +++ b/packages/twenty-cli/schemas/app-manifest.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json", + "title": "Twenty App Manifest", + "description": "Schema for Twenty application manifest files", + "type": "object", + "required": ["standardId", "label", "version"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference for validation and IDE support" + }, + "standardId": { + "type": "string", + "description": "Unique identifier for the application (UUID format recommended)", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + }, + "label": { + "type": "string", + "description": "Human-readable display name for the application", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": "string", + "description": "Brief description of what the application does", + "maxLength": 1000 + }, + "icon": { + "type": "string", + "description": "Icon for the application (emoji or icon name)", + "maxLength": 50 + }, + "version": { + "type": "string", + "description": "Semantic version of the application", + "pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9-]+)?$" + }, + "agents": { + "type": "array", + "description": "Optional inline agent definitions (agents are typically discovered from the agents/ folder)", + "items": { + "type": "object", + "description": "Inline agent definition", + "$ref": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json" + } + } + }, + "additionalProperties": false, + "examples": [ + { + "standardId": "550e8400-e29b-41d4-a716-446655440000", + "label": "Customer Support App", + "description": "Comprehensive customer support application with AI agents", + "icon": "🎧", + "version": "1.0.0" + } + ] +} diff --git a/packages/twenty-cli/src/cli.ts b/packages/twenty-cli/src/cli.ts new file mode 100644 index 0000000000..7575f562ae --- /dev/null +++ b/packages/twenty-cli/src/cli.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import { Command } from 'commander'; +import { AppCommand } from './commands/app.command'; +import { AuthCommand } from './commands/auth.command'; +import { ConfigCommand } from './commands/config.command'; + +const program = new Command(); + +program + .name('twenty') + .description('CLI for Twenty application development') + .version('0.1.0'); + +program + .option('-v, --verbose', 'Enable verbose logging') + .option( + '--api-url ', + 'Twenty API URL', + process.env.TWENTY_API_URL || 'http://localhost:3000', + ); + +program.addCommand(new AuthCommand().getCommand()); +program.addCommand(new AppCommand().getCommand()); +program.addCommand(new ConfigCommand().getCommand()); + +program.exitOverride(); + +try { + program.parse(); +} catch (error) { + if (error instanceof Error) { + console.error(chalk.red('Error:'), error.message); + process.exit(1); + } +} diff --git a/packages/twenty-cli/src/commands/app-deploy.command.ts b/packages/twenty-cli/src/commands/app-deploy.command.ts new file mode 100644 index 0000000000..b7eaecd535 --- /dev/null +++ b/packages/twenty-cli/src/commands/app-deploy.command.ts @@ -0,0 +1,33 @@ +import chalk from 'chalk'; +import { ApiService } from '../services/api.service'; +import { resolveAppPath } from '../utils/app-path-resolver'; +import { syncApp } from '../utils/app-sync'; + +export class AppDeployCommand { + private apiService = new ApiService(); + + async execute(options: { path?: string }): Promise { + try { + const appPath = await resolveAppPath(options.path); + + console.log(chalk.blue('πŸš€ Deploying Twenty Application')); + console.log(chalk.gray(`πŸ“ App Path: ${appPath}`)); + console.log(''); + + const result = await syncApp(appPath, this.apiService); + + if (!result.success) { + console.error(chalk.red('❌ Deployment failed:'), result.error); + process.exit(1); + } + + console.log(chalk.green('βœ… Application deployed successfully')); + } catch (error) { + console.error( + chalk.red('Deployment failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } +} diff --git a/packages/twenty-cli/src/commands/app-dev.command.ts b/packages/twenty-cli/src/commands/app-dev.command.ts new file mode 100644 index 0000000000..3548fd2274 --- /dev/null +++ b/packages/twenty-cli/src/commands/app-dev.command.ts @@ -0,0 +1,98 @@ +import chalk from 'chalk'; +import * as chokidar from 'chokidar'; +import { ApiService } from '../services/api.service'; +import { resolveAppPath } from '../utils/app-path-resolver'; +import { syncApp } from '../utils/app-sync'; + +export class AppDevCommand { + private apiService = new ApiService(); + + async execute(options: { + path?: string; + debounce: string; + verbose?: boolean; + }): Promise { + try { + const appPath = await resolveAppPath(options.path, options.verbose); + const debounceMs = parseInt(options.debounce, 10); + + this.logStartupInfo(appPath, debounceMs, options.verbose); + + await syncApp(appPath, this.apiService); + + const watcher = this.setupFileWatcher( + appPath, + debounceMs, + options.verbose, + ); + + this.setupGracefulShutdown(watcher); + } catch (error) { + console.error( + chalk.red('Development mode failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private logStartupInfo( + appPath: string, + debounceMs: number, + verbose?: boolean, + ): void { + console.log(chalk.blue('πŸš€ Starting Twenty Application Development Mode')); + console.log(chalk.gray(`πŸ“ App Path: ${appPath}`)); + console.log(chalk.gray(`⏱️ Debounce: ${debounceMs}ms`)); + console.log(chalk.gray(`πŸ”§ Verbose: ${verbose ? 'On' : 'Off'}`)); + console.log(''); + } + + private setupFileWatcher( + appPath: string, + debounceMs: number, + verbose?: boolean, + ): chokidar.FSWatcher { + const watcher = chokidar.watch(appPath, { + ignored: /node_modules|\.git/, + persistent: true, + }); + + let timeout: NodeJS.Timeout | null = null; + + const debouncedSync = () => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(async () => { + console.log(chalk.blue('πŸ”„ Changes detected, syncing...')); + await syncApp(appPath, this.apiService); + console.log( + chalk.gray('πŸ‘€ Watching for changes... (Press Ctrl+C to stop)'), + ); + }, debounceMs); + }; + + watcher.on('change', (filePath) => { + if (verbose) { + console.log(chalk.gray(`πŸ“ ${filePath} changed`)); + } + debouncedSync(); + }); + + console.log( + chalk.gray('πŸ‘€ Watching for changes... (Press Ctrl+C to stop)'), + ); + + return watcher; + } + + private setupGracefulShutdown(watcher: chokidar.FSWatcher): void { + process.on('SIGINT', () => { + console.log(chalk.yellow('\nπŸ›‘ Stopping development mode...')); + watcher.close(); + process.exit(0); + }); + } +} diff --git a/packages/twenty-cli/src/commands/app-init.command.ts b/packages/twenty-cli/src/commands/app-init.command.ts new file mode 100644 index 0000000000..b42f4fc422 --- /dev/null +++ b/packages/twenty-cli/src/commands/app-init.command.ts @@ -0,0 +1,118 @@ +import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import inquirer from 'inquirer'; +import * as path from 'path'; +import { + createAgentManifest, + createManifest, + createReadmeContent, +} from '../utils/app-template'; +import { writeJsoncFile } from '../utils/jsonc-parser'; + +export class AppInitCommand { + async execute(options: { path?: string; name?: string }): Promise { + try { + const appName = await this.getAppName(options.name); + const appDir = this.determineAppDirectory(options.path, appName); + + await this.validateDirectory(appDir); + + this.logCreationInfo(appDir, appName); + + await this.createAppStructure(appDir, appName); + + this.logSuccess(appDir); + } catch (error) { + console.error( + chalk.red('Initialization failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async getAppName(providedName?: string): Promise { + if (providedName) { + return providedName; + } + + const nameAnswer = await inquirer.prompt([ + { + type: 'input', + name: 'appName', + message: 'Application name:', + validate: (input) => { + if (input.length === 0) return 'Application name is required'; + if (!/^[a-z0-9-]+$/.test(input)) + return 'Name must contain only lowercase letters, numbers, and hyphens'; + return true; + }, + }, + ]); + + return nameAnswer.appName; + } + + private determineAppDirectory( + providedPath?: string, + appName?: string, + ): string { + if (providedPath) { + return path.resolve(providedPath); + } + + return path.join(process.cwd(), appName!); + } + + private async validateDirectory(appDir: string): Promise { + if (!(await fs.pathExists(appDir))) { + return; + } + + const files = await fs.readdir(appDir); + if (files.length > 0) { + throw new Error(`Directory ${appDir} already exists and is not empty`); + } + } + + private logCreationInfo(appDir: string, appName: string): void { + console.log(chalk.blue('🎯 Creating Twenty Application')); + console.log(chalk.gray(`πŸ“ Directory: ${appDir}`)); + console.log(chalk.gray(`πŸ“ Name: ${appName}`)); + console.log(''); + } + + private async createAppStructure( + appDir: string, + appName: string, + ): Promise { + await fs.ensureDir(appDir); + + // Create agents directory + const agentsDir = path.join(appDir, 'agents'); + await fs.ensureDir(agentsDir); + + // Create main manifest with agent references + const manifest = createManifest(appName); + const manifestPath = path.join(appDir, 'twenty-app.jsonc'); + await writeJsoncFile(manifestPath, manifest); + + // Create agent manifest file + const agentManifest = createAgentManifest(appName); + const agentFileName = `${appName}-agent`; + const agentPath = path.join(agentsDir, `${agentFileName}.jsonc`); + await writeJsoncFile(agentPath, agentManifest); + + // Create README + const readmeContent = createReadmeContent(appName, appDir); + await fs.writeFile(path.join(appDir, 'README.md'), readmeContent); + } + + private logSuccess(appDir: string): void { + console.log(chalk.green('βœ… Application created successfully!')); + console.log(''); + console.log(chalk.blue('Next steps:')); + console.log(` cd ${appDir}`); + console.log(' twenty app dev'); + } +} diff --git a/packages/twenty-cli/src/commands/app-install.command.ts b/packages/twenty-cli/src/commands/app-install.command.ts new file mode 100644 index 0000000000..a103ad82d1 --- /dev/null +++ b/packages/twenty-cli/src/commands/app-install.command.ts @@ -0,0 +1,57 @@ +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { ApiService } from '../services/api.service'; + +export class AppInstallCommand { + private apiService = new ApiService(); + + async execute(options: { source?: string; type: string }): Promise { + try { + const source = await this.getSource(options.source); + + this.logInstallInfo(source, options.type); + + const result = await this.apiService.installApplication( + source, + options.type as 'local' | 'git' | 'marketplace', + ); + + if (!result.success) { + console.error(chalk.red('❌ Installation failed:'), result.error); + process.exit(1); + } + + console.log(chalk.green('βœ… Application installed successfully')); + } catch (error) { + console.error( + chalk.red('Installation failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async getSource(providedSource?: string): Promise { + if (providedSource) { + return providedSource; + } + + const answer = await inquirer.prompt([ + { + type: 'input', + name: 'source', + message: 'Application source (URL, path, or ID):', + validate: (input) => input.length > 0 || 'Source is required', + }, + ]); + + return answer.source; + } + + private logInstallInfo(source: string, type: string): void { + console.log(chalk.blue('πŸ“¦ Installing Twenty Application')); + console.log(chalk.gray(`πŸ“ Source: ${source}`)); + console.log(chalk.gray(`πŸ”§ Type: ${type}`)); + console.log(''); + } +} diff --git a/packages/twenty-cli/src/commands/app-list.command.ts b/packages/twenty-cli/src/commands/app-list.command.ts new file mode 100644 index 0000000000..df95a7fecf --- /dev/null +++ b/packages/twenty-cli/src/commands/app-list.command.ts @@ -0,0 +1,45 @@ +import chalk from 'chalk'; +import { ApiService } from '../services/api.service'; + +export class AppListCommand { + private apiService = new ApiService(); + + async execute(): Promise { + try { + console.log(chalk.blue('πŸ“‹ Listing Twenty Applications')); + console.log(''); + + const result = await this.apiService.listApplications(); + + if (!result.success || !result.data) { + console.error( + chalk.red('❌ Failed to list applications:'), + result.error, + ); + process.exit(1); + } + + this.displayApplications(result.data); + } catch (error) { + console.error( + chalk.red('List failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private displayApplications(apps: any[]): void { + if (apps.length === 0) { + console.log(chalk.yellow('No applications found')); + return; + } + + apps.forEach((app: any, index: number) => { + console.log(`${index + 1}. ${chalk.bold(app.name)}`); + console.log(` ${chalk.gray(app.description || 'No description')}`); + console.log(` ${chalk.gray(`Version: ${app.version || 'N/A'}`)}`); + console.log(''); + }); + } +} diff --git a/packages/twenty-cli/src/commands/app.command.ts b/packages/twenty-cli/src/commands/app.command.ts new file mode 100644 index 0000000000..4fbffd2ddf --- /dev/null +++ b/packages/twenty-cli/src/commands/app.command.ts @@ -0,0 +1,77 @@ +import { Command } from 'commander'; +import { AppDeployCommand } from './app-deploy.command'; +import { AppDevCommand } from './app-dev.command'; +import { AppInitCommand } from './app-init.command'; +import { AppInstallCommand } from './app-install.command'; +import { AppListCommand } from './app-list.command'; + +export class AppCommand { + private devCommand = new AppDevCommand(); + private deployCommand = new AppDeployCommand(); + private installCommand = new AppInstallCommand(); + private listCommand = new AppListCommand(); + private initCommand = new AppInitCommand(); + + getCommand(): Command { + const appCommand = new Command('app'); + appCommand.description('Application development commands'); + + appCommand + .command('dev') + .description('Watch and sync local application changes') + .option( + '-p, --path ', + 'Application directory path (auto-detected if not specified)', + ) + .option('-d, --debounce ', 'Debounce delay in milliseconds', '1000') + .option('--verbose', 'Enable verbose logging') + .action(async (options) => { + await this.devCommand.execute(options); + }); + + appCommand + .command('deploy') + .description('Deploy application to Twenty') + .option( + '-p, --path ', + 'Application directory path (auto-detected if not specified)', + ) + .action(async (options) => { + await this.deployCommand.execute(options); + }); + + appCommand + .command('install') + .description('Install application from source') + .option( + '-s, --source ', + 'Application source (git URL, local path, or marketplace ID)', + ) + .option( + '-t, --type ', + 'Source type (git, local, marketplace)', + 'local', + ) + .action(async (options) => { + await this.installCommand.execute(options); + }); + + appCommand + .command('list') + .description('List installed applications') + .action(async () => { + await this.listCommand.execute(); + }); + + appCommand + .command('init') + .description('Initialize a new Twenty application') + .option('-p, --path ', 'Directory to create the application in') + .option('-n, --name ', 'Application name') + .action(async (options) => { + await this.initCommand.execute(options); + }); + + return appCommand; + } +} diff --git a/packages/twenty-cli/src/commands/auth.command.ts b/packages/twenty-cli/src/commands/auth.command.ts new file mode 100644 index 0000000000..5243cbedd5 --- /dev/null +++ b/packages/twenty-cli/src/commands/auth.command.ts @@ -0,0 +1,150 @@ +import chalk from 'chalk'; +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import { ApiService } from '../services/api.service'; +import { ConfigService } from '../services/config.service'; + +export class AuthCommand { + private configService = new ConfigService(); + private apiService = new ApiService(); + + getCommand(): Command { + const authCommand = new Command('auth'); + authCommand.description('Authentication commands'); + + authCommand + .command('login') + .description('Authenticate with Twenty') + .option('--api-key ', 'API key for authentication') + .option('--api-url ', 'Twenty API URL') + .action(async (options) => { + await this.login(options); + }); + + authCommand + .command('logout') + .description('Remove authentication credentials') + .action(async () => { + await this.logout(); + }); + + authCommand + .command('status') + .description('Check authentication status') + .action(async () => { + await this.status(); + }); + + return authCommand; + } + + private async login(options: { + apiKey?: string; + apiUrl?: string; + }): Promise { + try { + let { apiKey, apiUrl } = options; + + // Get current config + const config = await this.configService.getConfig(); + + // Prompt for missing values + if (!apiUrl) { + const urlAnswer = await inquirer.prompt([ + { + type: 'input', + name: 'apiUrl', + message: 'Twenty API URL:', + default: config.apiUrl, + validate: (input) => { + try { + new URL(input); + return true; + } catch { + return 'Please enter a valid URL'; + } + }, + }, + ]); + apiUrl = urlAnswer.apiUrl; + } + + if (!apiKey) { + const keyAnswer = await inquirer.prompt([ + { + type: 'password', + name: 'apiKey', + message: 'API Key:', + mask: '*', + validate: (input) => input.length > 0 || 'API key is required', + }, + ]); + apiKey = keyAnswer.apiKey; + } + + // Update config + await this.configService.setConfig({ + apiUrl, + apiKey, + }); + + // Validate authentication + const isValid = await this.apiService.validateAuth(); + + if (isValid) { + console.log(chalk.green('βœ“ Successfully authenticated with Twenty')); + } else { + console.log( + chalk.red('βœ— Authentication failed. Please check your credentials.'), + ); + process.exit(1); + } + } catch (error) { + console.error( + chalk.red('Login failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async logout(): Promise { + try { + await this.configService.clearConfig(); + console.log(chalk.green('βœ“ Successfully logged out')); + } catch (error) { + console.error( + chalk.red('Logout failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async status(): Promise { + try { + const config = await this.configService.getConfig(); + + console.log(chalk.blue('Authentication Status:')); + console.log(`API URL: ${config.apiUrl}`); + console.log( + `API Key: ${config.apiKey ? '***' + config.apiKey.slice(-4) : 'Not set'}`, + ); + + if (config.apiKey) { + const isValid = await this.apiService.validateAuth(); + console.log( + `Status: ${isValid ? chalk.green('βœ“ Valid') : chalk.red('βœ— Invalid')}`, + ); + } else { + console.log(`Status: ${chalk.yellow('⚠ Not authenticated')}`); + } + } catch (error) { + console.error( + chalk.red('Status check failed:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } +} diff --git a/packages/twenty-cli/src/commands/config.command.ts b/packages/twenty-cli/src/commands/config.command.ts new file mode 100644 index 0000000000..01a1fa0828 --- /dev/null +++ b/packages/twenty-cli/src/commands/config.command.ts @@ -0,0 +1,140 @@ +import chalk from 'chalk'; +import { Command } from 'commander'; +import { ConfigService } from '../services/config.service'; +import { TwentyConfig } from '../types/config.types'; + +export class ConfigCommand { + private configService = new ConfigService(); + + getCommand(): Command { + const configCommand = new Command('config'); + configCommand.description('Configuration management'); + + configCommand + .command('get [key]') + .description('Get configuration value(s)') + .action(async (key) => { + await this.get(key); + }); + + configCommand + .command('set ') + .description('Set configuration value') + .action(async (key, value) => { + await this.set(key, value); + }); + + configCommand + .command('unset ') + .description('Remove configuration value') + .action(async (key) => { + await this.unset(key); + }); + + configCommand + .command('list') + .description('List all configuration values') + .action(async () => { + await this.list(); + }); + + return configCommand; + } + + private async get(key: string | undefined): Promise { + try { + const config = await this.configService.getConfig(); + + if (key) { + const value = (config as any)[key]; + if (value !== undefined) { + console.log(value); + } else { + console.log(chalk.gray('(not set)')); + } + } else { + this.printConfig('Configuration', config); + } + } catch (error) { + console.error( + chalk.red('Failed to get configuration:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async set(key: keyof TwentyConfig, value: string): Promise { + try { + const config = await this.configService.getConfig(); + config[key] = value; + await this.configService.setConfig(config); + console.log(chalk.green(`βœ“ Set ${key} in configuration`)); + } catch (error) { + console.error( + chalk.red('Failed to set configuration:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async unset(key: string): Promise { + try { + const config = await this.configService.getConfig(); + delete (config as any)[key]; + await this.configService.setConfig(config); + console.log(chalk.green(`βœ“ Removed ${key} from configuration`)); + } catch (error) { + console.error( + chalk.red('Failed to unset configuration:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private async list(): Promise { + try { + const config = await this.configService.getConfig(); + this.printConfig('Configuration', config); + } catch (error) { + console.error( + chalk.red('Failed to list configuration:'), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } + } + + private printConfig(title: string, config: Record): void { + console.log(chalk.blue(title + ':')); + + const keys = Object.keys(config); + if (keys.length === 0) { + console.log(chalk.gray(' (empty)')); + return; + } + + keys.forEach((key) => { + const value = config[key]; + const displayValue = + key.toLowerCase().includes('key') && value + ? '***' + value.slice(-4) + : value; + + // Show if value is overridden by environment variable + const envVarName = `TWENTY_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`; + const isOverridden = process.env[envVarName] !== undefined; + const suffix = isOverridden ? chalk.gray(' (from env)') : ''; + + console.log(` ${key}: ${displayValue}${suffix}`); + }); + + // Show available environment variables + console.log(chalk.gray('\nEnvironment variables:')); + console.log(chalk.gray(' TWENTY_API_URL - Override API URL')); + console.log(chalk.gray(' TWENTY_API_KEY - Override API key')); + console.log(chalk.gray(' TWENTY_DEFAULT_APP - Override default app')); + } +} diff --git a/packages/twenty-cli/src/services/api.service.ts b/packages/twenty-cli/src/services/api.service.ts new file mode 100644 index 0000000000..668bebde2b --- /dev/null +++ b/packages/twenty-cli/src/services/api.service.ts @@ -0,0 +1,260 @@ +import axios, { type AxiosInstance, type AxiosResponse } from 'axios'; +import chalk from 'chalk'; +import { type ApiResponse, type AppManifest } from '../types/config.types'; +import { ConfigService } from './config.service'; + +export class ApiService { + private client: AxiosInstance; + private configService: ConfigService; + + constructor() { + this.configService = new ConfigService(); + this.client = axios.create(); + + this.client.interceptors.request.use(async (config) => { + const twentyConfig = await this.configService.getConfig(); + + config.baseURL = twentyConfig.apiUrl; + + if (twentyConfig.apiKey) { + config.headers.Authorization = `Bearer ${twentyConfig.apiKey}`; + } + + return config; + }); + + this.client.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + console.error( + chalk.red( + 'Authentication failed. Please run `twenty auth login` first.', + ), + ); + } else if (error.response?.status === 403) { + console.error( + chalk.red( + 'Access denied. Check your API key and workspace permissions.', + ), + ); + } else if (error.code === 'ECONNREFUSED') { + console.error( + chalk.red('Cannot connect to Twenty server. Is it running?'), + ); + } + throw error; + }, + ); + } + + async validateAuth(): Promise { + try { + const query = ` + query FindManyAgents { + findManyAgents { + id + name + } + } + `; + + const response = await this.client.post( + '/metadata', + { + query, + }, + { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + 'x-schema-version': '6', + }, + }, + ); + + return response.status === 200 && !response.data.errors; + } catch { + return false; + } + } + + async syncApplication(manifest: AppManifest): Promise { + try { + const mutation = ` + mutation SyncApplication($manifest: JSON!) { + syncApplication(manifest: $manifest) { + id + standardId + label + description + version + createdAt + updatedAt + } + } + `; + + const variables = { + manifest, + }; + + const response: AxiosResponse = await this.client.post( + '/metadata', + { + query: mutation, + variables, + }, + { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + 'x-schema-version': '6', + }, + }, + ); + + if (response.data.errors) { + return { + success: false, + error: + response.data.errors[0]?.message || 'Failed to sync application', + }; + } + + return { + success: true, + data: response.data.data.syncApplication, + message: `Successfully synced application: ${manifest.label}`, + }; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + return { + success: false, + error: error.response.data?.errors?.[0]?.message || error.message, + }; + } + throw error; + } + } + + async installApplication( + source: string, + sourceType: 'git' | 'local' | 'marketplace' = 'local', + ): Promise { + // For now, installation is the same as syncing a local manifest + // In the future, this could handle different source types + try { + if (sourceType === 'local') { + // Try to load manifest using the new loader + try { + const { loadAppManifest } = await import( + '../utils/app-manifest-loader' + ); + const manifest = await loadAppManifest(source); + return this.syncApplication(manifest); + } catch (manifestError) { + return { + success: false, + error: `Failed to load manifest: ${manifestError instanceof Error ? manifestError.message : 'Unknown error'}`, + }; + } + } + + return { + success: false, + error: `Source type "${sourceType}" not yet supported`, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Installation failed', + }; + } + } + + async listApplications(): Promise { + try { + const query = ` + query FindManyAgents { + findManyAgents { + id + name + label + description + isCustom + createdAt + updatedAt + } + } + `; + + const response: AxiosResponse = await this.client.post('/metadata', { + query, + }); + + if (response.data.errors) { + return { + success: false, + error: response.data.errors[0]?.message || 'Failed to fetch agents', + }; + } + + return { + success: true, + data: response.data.data.findManyAgents, + }; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + return { + success: false, + error: error.response.data?.errors?.[0]?.message || error.message, + }; + } + throw error; + } + } + + async getWorkspaces(): Promise { + try { + const query = ` + query CurrentUser { + currentUser { + id + email + currentWorkspace { + id + displayName + } + } + } + `; + + const response: AxiosResponse = await this.client.post('/metadata', { + query, + }); + + if (response.data.errors) { + return { + success: false, + error: + response.data.errors[0]?.message || 'Failed to fetch workspace', + }; + } + + const workspace = response.data.data.currentUser?.currentWorkspace; + return { + success: true, + data: workspace ? [workspace] : [], + }; + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + return { + success: false, + error: error.response.data?.errors?.[0]?.message || error.message, + }; + } + throw error; + } + } +} diff --git a/packages/twenty-cli/src/services/config.service.ts b/packages/twenty-cli/src/services/config.service.ts new file mode 100644 index 0000000000..ccc9d0fcb5 --- /dev/null +++ b/packages/twenty-cli/src/services/config.service.ts @@ -0,0 +1,96 @@ +import { config as loadDotenv } from 'dotenv'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { TwentyConfig } from '../types/config.types'; + +export class ConfigService { + private configPath: string; + + constructor() { + this.configPath = path.join(os.homedir(), '.twenty', 'config.json'); + this.loadEnvironmentVariables(); + } + + private loadEnvironmentVariables(): void { + // Load local .env file if it exists in current working directory + const localEnvPath = path.join(process.cwd(), '.env'); + if (fs.existsSync(localEnvPath)) { + loadDotenv({ path: localEnvPath }); + } + + // Also try to load from user's home .twenty directory + const userEnvPath = path.join(os.homedir(), '.twenty', '.env'); + if (fs.existsSync(userEnvPath)) { + loadDotenv({ path: userEnvPath }); + } + } + + async getConfig(): Promise { + try { + // Start with default config + const defaultConfig = this.getDefaultConfig(); + + // Load config file if it exists + let fileConfig = {}; + await fs.ensureFile(this.configPath); + const configExists = await fs.pathExists(this.configPath); + + if (configExists) { + const configContent = await fs.readFile(this.configPath, 'utf8'); + fileConfig = JSON.parse(configContent || '{}'); + } + + // Environment variables override everything + const envConfig = this.getEnvironmentConfig(); + + // Merge configs with proper precedence: defaults < file < environment + return { + ...defaultConfig, + ...fileConfig, + ...envConfig, + }; + } catch { + return this.getDefaultConfig(); + } + } + + async setConfig(config: Partial): Promise { + const currentConfig = await this.getConfig(); + const newConfig = { ...currentConfig, ...config }; + + await fs.ensureDir(path.dirname(this.configPath)); + await fs.writeFile(this.configPath, JSON.stringify(newConfig, null, 2)); + } + + async clearConfig(): Promise { + const configExists = await fs.pathExists(this.configPath); + if (configExists) { + await fs.remove(this.configPath); + } + } + + private getDefaultConfig(): TwentyConfig { + return { + apiUrl: 'http://localhost:3000', + }; + } + + private getEnvironmentConfig(): Partial { + const envConfig: Partial = {}; + + if (process.env.TWENTY_API_URL) { + envConfig.apiUrl = process.env.TWENTY_API_URL; + } + + if (process.env.TWENTY_API_KEY) { + envConfig.apiKey = process.env.TWENTY_API_KEY; + } + + if (process.env.TWENTY_DEFAULT_APP) { + envConfig.defaultApp = process.env.TWENTY_DEFAULT_APP; + } + + return envConfig; + } +} diff --git a/packages/twenty-cli/src/types/config.types.ts b/packages/twenty-cli/src/types/config.types.ts new file mode 100644 index 0000000000..a89bdcf6bc --- /dev/null +++ b/packages/twenty-cli/src/types/config.types.ts @@ -0,0 +1,37 @@ +export interface TwentyConfig { + apiUrl: string; + apiKey?: string; + defaultApp?: string; +} + +export interface AppManifest { + standardId: string; + label: string; + description?: string; + icon?: string; + version: string; + agents: AgentManifest[]; +} + +export interface AgentManifest { + standardId: string; + name: string; + label: string; + description?: string; + icon?: string; + prompt: string; + modelId?: string; + responseFormat?: AgentResponseFormat; +} + +export interface AgentResponseFormat { + type: 'json' | 'text'; + schema?: Record; +} + +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} diff --git a/packages/twenty-cli/src/utils/__tests__/app-template.test.ts b/packages/twenty-cli/src/utils/__tests__/app-template.test.ts new file mode 100644 index 0000000000..8563eba99f --- /dev/null +++ b/packages/twenty-cli/src/utils/__tests__/app-template.test.ts @@ -0,0 +1,125 @@ +import { + createAgentManifest, + createManifest, + createReadmeContent, +} from '../app-template'; + +// Mock crypto.randomUUID to make tests deterministic +jest.mock('crypto', () => ({ + randomUUID: jest.fn(() => 'mocked-uuid-12345'), +})); + +describe('app-template', () => { + describe('createManifest', () => { + it('should create a valid app manifest with correct structure', () => { + const appName = 'my-test-app'; + const manifest = createManifest(appName); + + expect(manifest).toEqual({ + $schema: + 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json', + standardId: 'mocked-uuid-12345', + label: 'My Test App', + description: 'A Twenty application for my-test-app', + version: '1.0.0', + // agents will be discovered from the agents/ folder + }); + }); + + it('should handle single word app names', () => { + const appName = 'calculator'; + const manifest = createManifest(appName); + + expect(manifest.label).toBe('Calculator'); + expect(manifest.standardId).toBe('mocked-uuid-12345'); + }); + + it('should handle kebab-case app names correctly', () => { + const appName = 'user-management-system'; + const manifest = createManifest(appName); + + expect(manifest.label).toBe('User Management System'); + expect(manifest.standardId).toBe('mocked-uuid-12345'); + }); + + it('should generate unique standardIds', () => { + const manifest = createManifest('test-app'); + + expect(manifest.standardId).toBeDefined(); + expect(typeof manifest.standardId).toBe('string'); + }); + }); + + describe('createAgentManifest', () => { + it('should create a valid agent manifest with correct structure', () => { + const appName = 'my-test-app'; + const agent = createAgentManifest(appName); + + expect(agent).toEqual({ + $schema: + 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json', + standardId: 'mocked-uuid-12345', + name: 'myTestAppAgent', + label: 'My Test App Agent', + description: 'AI agent for my-test-app', + prompt: + 'You are an AI agent for my-test-app. Help users with their tasks and provide assistance with Twenty CRM features.', + modelId: 'auto', + responseFormat: { + type: 'text', + }, + }); + }); + + it('should handle single word app names', () => { + const appName = 'calculator'; + const agent = createAgentManifest(appName); + + expect(agent.name).toBe('calculatorAgent'); + expect(agent.label).toBe('Calculator Agent'); + }); + + it('should handle kebab-case app names correctly', () => { + const appName = 'user-management-system'; + const agent = createAgentManifest(appName); + + expect(agent.name).toBe('userManagementSystemAgent'); + expect(agent.label).toBe('User Management System Agent'); + }); + }); + + describe('createReadmeContent', () => { + it('should generate correct README content', () => { + const appName = 'my-awesome-app'; + const appDir = '/path/to/my-awesome-app'; + const readmeContent = createReadmeContent(appName, appDir); + + expect(readmeContent).toContain('# my-awesome-app'); + expect(readmeContent).toContain('A Twenty application.'); + expect(readmeContent).toContain( + 'twenty app dev --path /path/to/my-awesome-app', + ); + expect(readmeContent).toContain('cd /path/to/my-awesome-app'); + expect(readmeContent).toContain( + 'twenty app deploy --path /path/to/my-awesome-app', + ); + }); + + it('should include development and deployment sections', () => { + const readmeContent = createReadmeContent('test-app', '/test/path'); + + expect(readmeContent).toContain('## Development'); + expect(readmeContent).toContain('## Deployment'); + expect(readmeContent).toContain('To start development mode:'); + expect(readmeContent).toContain('To deploy the application:'); + }); + + it('should handle different app directories', () => { + const appName = 'sample-app'; + const appDir = '/custom/directory/sample-app'; + const readmeContent = createReadmeContent(appName, appDir); + + expect(readmeContent).toContain('/custom/directory/sample-app'); + }); + }); +}); diff --git a/packages/twenty-cli/src/utils/app-discovery.ts b/packages/twenty-cli/src/utils/app-discovery.ts new file mode 100644 index 0000000000..553349201f --- /dev/null +++ b/packages/twenty-cli/src/utils/app-discovery.ts @@ -0,0 +1,78 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; + +export const findProjectRoot = async (): Promise => { + let currentDir = process.cwd(); + const maxDepth = 10; + let depth = 0; + + while (depth < maxDepth) { + const nxConfig = path.join(currentDir, 'nx.json'); + const packageJson = path.join(currentDir, 'package.json'); + + if (await fs.pathExists(nxConfig)) { + return currentDir; + } + + if (await fs.pathExists(packageJson)) { + try { + const pkg = await fs.readJson(packageJson); + if (pkg.workspaces || pkg.name === 'twenty') { + return currentDir; + } + } catch { + // Ignore JSON parse errors + } + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) break; + + currentDir = parentDir; + depth++; + } + + return null; +}; + +export const findNearbyApps = async (startDir: string): Promise => { + const apps: string[] = []; + + try { + const searchPaths = [ + startDir, + path.join(startDir, '..'), + path.join(startDir, '../..'), + path.join(startDir, 'packages/twenty-apps'), + path.join(startDir, '../../packages/twenty-apps'), + ]; + + for (const searchPath of searchPaths) { + if (await fs.pathExists(searchPath)) { + const items = await fs.readdir(searchPath, { withFileTypes: true }); + + for (const item of items) { + if (item.isDirectory()) { + const manifestPath = path.join( + searchPath, + item.name, + 'twenty-app.json', + ); + if (await fs.pathExists(manifestPath)) { + apps.push(path.join(searchPath, item.name)); + } + } + } + } + } + } catch { + // Ignore errors during search + } + + return apps.slice(0, 5); +}; + +export const isValidAppPath = async (appPath: string): Promise => { + const manifestPath = path.join(appPath, 'twenty-app.json'); + return fs.pathExists(manifestPath); +}; diff --git a/packages/twenty-cli/src/utils/app-manifest-loader.ts b/packages/twenty-cli/src/utils/app-manifest-loader.ts new file mode 100644 index 0000000000..6823ad4d15 --- /dev/null +++ b/packages/twenty-cli/src/utils/app-manifest-loader.ts @@ -0,0 +1,160 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { AgentManifest, AppManifest } from '../types/config.types'; +import { parseJsoncFile } from './jsonc-parser'; +import { schemaValidator } from './schema-validator'; + +export interface AppManifestWithMeta extends AppManifest { + _meta?: { + agentFiles?: string[]; + manifestPath?: string; + }; +} + +export type AppManifestRaw = Omit & { + // agents will be discovered from the agents/ folder + agents?: AgentManifest[]; +}; + +export class AppManifestLoader { + private appPath: string; + + constructor(appPath: string) { + this.appPath = appPath; + } + + async loadManifest(): Promise { + const manifestPath = await this.findManifestFile(); + const rawManifest = await parseJsoncFile(manifestPath); + + // Validate the raw manifest structure + await schemaValidator.validateAppManifest(rawManifest, manifestPath); + + return this.discoverAndLoadAgents(rawManifest, manifestPath); + } + + private async findManifestFile(): Promise { + // Try JSONC first, then fall back to JSON for backward compatibility + const jsoncPath = path.join(this.appPath, 'twenty-app.jsonc'); + const jsonPath = path.join(this.appPath, 'twenty-app.json'); + + if (await fs.pathExists(jsoncPath)) { + return jsoncPath; + } + + if (await fs.pathExists(jsonPath)) { + return jsonPath; + } + + throw new Error( + `No manifest file found. Expected twenty-app.jsonc or twenty-app.json in ${this.appPath}`, + ); + } + + private async discoverAndLoadAgents( + rawManifest: AppManifestRaw, + manifestPath: string, + ): Promise { + const agentsDir = path.join(this.appPath, 'agents'); + const agentFiles: string[] = []; + const agents: AgentManifest[] = []; + + // Check if agents directory exists + if (await fs.pathExists(agentsDir)) { + const files = await fs.readdir(agentsDir); + const agentFileNames = files.filter( + (file) => file.endsWith('.jsonc') || file.endsWith('.json'), + ); + + for (const fileName of agentFileNames) { + const agentPath = path.join(agentsDir, fileName); + const agentManifest = await parseJsoncFile(agentPath); + + // Validate the agent against schema + await schemaValidator.validateAgent(agentManifest, agentPath); + + agents.push(agentManifest); + agentFiles.push(`agents/${fileName}`); + } + } + + return { + standardId: rawManifest.standardId, + label: rawManifest.label, + description: rawManifest.description, + icon: rawManifest.icon, + version: rawManifest.version, + agents, + _meta: { + agentFiles, + manifestPath, + }, + }; + } + + // Utility method to split agents from an existing manifest + static async splitAgentsFromManifest( + appPath: string, + options: { + agentsDir?: string; + preserveOriginal?: boolean; + } = {}, + ): Promise { + const loader = new AppManifestLoader(appPath); + const manifest = await loader.loadManifest(); + + const agentsDir = options.agentsDir || 'agents'; + const agentsDirPath = path.join(appPath, agentsDir); + + // Create agents directory + await fs.ensureDir(agentsDirPath); + + // Extract agents to separate files + for (const agent of manifest.agents) { + const agentFileName = `${agent.name}.jsonc`; + const agentFilePath = path.join(agentsDirPath, agentFileName); + + // Write agent to separate file + await fs.writeFile(agentFilePath, JSON.stringify(agent, null, 2), 'utf8'); + } + + // Update main manifest (remove agents array since they're now discovered) + const updatedManifest = { + standardId: manifest.standardId, + label: manifest.label, + description: manifest.description, + icon: manifest.icon, + version: manifest.version, + // No agents array - they will be discovered from the agents/ folder + }; + + // Write updated manifest as JSONC + const newManifestPath = path.join(appPath, 'twenty-app.jsonc'); + await fs.writeFile( + newManifestPath, + JSON.stringify(updatedManifest, null, 2), + 'utf8', + ); + + // Optionally remove original JSON file + if (!options.preserveOriginal) { + const oldManifestPath = path.join(appPath, 'twenty-app.json'); + if (await fs.pathExists(oldManifestPath)) { + await fs.remove(oldManifestPath); + } + } + } +} + +// Convenience function for backward compatibility +export const loadAppManifest = async ( + appPath: string, +): Promise => { + const loader = new AppManifestLoader(appPath); + const manifest = await loader.loadManifest(); + + // Remove meta information for backward compatibility + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _meta, ...cleanManifest } = manifest; + return cleanManifest; +}; diff --git a/packages/twenty-cli/src/utils/app-path-resolver.ts b/packages/twenty-cli/src/utils/app-path-resolver.ts new file mode 100644 index 0000000000..d00462949b --- /dev/null +++ b/packages/twenty-cli/src/utils/app-path-resolver.ts @@ -0,0 +1,116 @@ +import chalk from 'chalk'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { + findNearbyApps, + findProjectRoot, + isValidAppPath, +} from './app-discovery'; + +export const resolveAppPath = async ( + providedPath?: string, + verbose = false, +): Promise => { + if (providedPath && path.isAbsolute(providedPath)) { + return validateAppPath(providedPath, verbose); + } + + if (providedPath) { + return resolveRelativePath(providedPath); + } + + return autoDetectAppPath(verbose); +}; + +const resolveRelativePath = async (providedPath: string): Promise => { + const fromCwd = path.resolve(process.cwd(), providedPath); + if (await isValidAppPath(fromCwd)) { + return fromCwd; + } + + const projectRoot = await findProjectRoot(); + if (projectRoot) { + const fromProjectRoot = path.resolve(projectRoot, providedPath); + if (await isValidAppPath(fromProjectRoot)) { + return fromProjectRoot; + } + } + + throw new Error(`Cannot find twenty-app.json at any of these locations: + - ${fromCwd} + - ${projectRoot ? path.resolve(projectRoot, providedPath) : 'N/A (no project root found)'} + +Please check the path or run from the correct directory.`); +}; + +const autoDetectAppPath = async (verbose = false): Promise => { + let currentDir = process.cwd(); + const maxDepth = 10; + let depth = 0; + + while (depth < maxDepth) { + if (await isValidAppPath(currentDir)) { + if (verbose) { + console.log(chalk.gray(`Auto-detected app path: ${currentDir}`)); + } + return currentDir; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) break; + + currentDir = parentDir; + depth++; + } + + const suggestions = await findNearbyApps(process.cwd()); + let errorMessage = + 'No twenty-app.json found in current directory or parent directories.'; + + if (suggestions.length > 0) { + errorMessage += '\n\nFound Twenty applications nearby:'; + suggestions.forEach((suggestion, i) => { + errorMessage += `\n ${i + 1}. ${suggestion}`; + }); + errorMessage += + '\n\nTry running from one of these directories or use --path option.'; + } else { + errorMessage += '\n\nRun `twenty app init` to create a new application.'; + } + + throw new Error(errorMessage); +}; + +const validateAppPath = async ( + appPath: string, + verbose = false, +): Promise => { + if (verbose) { + console.log(chalk.gray(`Checking app path: ${appPath}`)); + } + + const jsoncManifestPath = path.join(appPath, 'twenty-app.jsonc'); + const jsonManifestPath = path.join(appPath, 'twenty-app.json'); + + const hasJsoncManifest = await fs.pathExists(jsoncManifestPath); + const hasJsonManifest = await fs.pathExists(jsonManifestPath); + + if (!hasJsoncManifest && !hasJsonManifest) { + let errorMessage = `No manifest file found. Expected twenty-app.jsonc or twenty-app.json in: ${appPath}`; + + if (await fs.pathExists(appPath)) { + try { + const files = await fs.readdir(appPath); + errorMessage += `\n\nFiles in directory: ${files.join(', ')}`; + } catch { + errorMessage += '\n\nCould not read directory contents.'; + } + } else { + errorMessage += '\n\nDirectory does not exist.'; + } + + throw new Error(errorMessage); + } + + return appPath; +}; diff --git a/packages/twenty-cli/src/utils/app-sync.ts b/packages/twenty-cli/src/utils/app-sync.ts new file mode 100644 index 0000000000..a2058331f3 --- /dev/null +++ b/packages/twenty-cli/src/utils/app-sync.ts @@ -0,0 +1,28 @@ +import chalk from 'chalk'; +import { ApiService } from '../services/api.service'; +import { loadAppManifest } from './app-manifest-loader'; + +export const syncApp = async ( + appPath: string, + apiService: ApiService, +): Promise => { + const manifest = await loadAppManifest(appPath); + + try { + const result = await apiService.syncApplication(manifest); + + if (result.success) { + console.log(chalk.green('βœ… Application synced successfully')); + } else { + console.error(chalk.red('❌ Sync failed:'), result.error); + } + + return result; + } catch (error) { + console.error( + chalk.red('Sync error:'), + error instanceof Error ? error.message : error, + ); + throw error; + } +}; diff --git a/packages/twenty-cli/src/utils/app-template.ts b/packages/twenty-cli/src/utils/app-template.ts new file mode 100644 index 0000000000..32e5ce3dba --- /dev/null +++ b/packages/twenty-cli/src/utils/app-template.ts @@ -0,0 +1,81 @@ +import { randomUUID } from 'crypto'; +import { AgentManifest, AppManifest } from '../types/config.types'; +import { SchemaValidator } from './schema-validator'; + +export type AppManifestTemplate = Omit & { + $schema?: string; + // agents will be discovered from the agents/ folder +}; + +export type AgentManifestTemplate = AgentManifest & { + $schema?: string; +}; + +export const createManifest = (appName: string): AppManifestTemplate => { + const schemas = SchemaValidator.getSchemaUrls(); + + return { + $schema: schemas.appManifest, + standardId: randomUUID(), + label: appName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '), + description: `A Twenty application for ${appName}`, + version: '1.0.0', + // agents will be discovered from the agents/ folder + }; +}; + +export const createAgentManifest = (appName: string): AgentManifestTemplate => { + const schemas = SchemaValidator.getSchemaUrls(); + + return { + $schema: schemas.agent, + standardId: randomUUID(), + name: `${appName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())}Agent`, + label: `${appName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ')} Agent`, + description: `AI agent for ${appName}`, + prompt: `You are an AI agent for ${appName}. Help users with their tasks and provide assistance with Twenty CRM features.`, + modelId: 'auto', + responseFormat: { + type: 'text', + }, + }; +}; + +export const createReadmeContent = ( + appName: string, + appDir: string, +): string => { + return `# ${appName} + +A Twenty application. + +## Development + +To start development mode: + +\`\`\`bash +twenty app dev --path ${appDir} +\`\`\` + +Or from the app directory: + +\`\`\`bash +cd ${appDir} +twenty app dev +\`\`\` + +## Deployment + +To deploy the application: + +\`\`\`bash +twenty app deploy --path ${appDir} +\`\`\` +`; +}; diff --git a/packages/twenty-cli/src/utils/jsonc-parser.ts b/packages/twenty-cli/src/utils/jsonc-parser.ts new file mode 100644 index 0000000000..67d13b84e8 --- /dev/null +++ b/packages/twenty-cli/src/utils/jsonc-parser.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs-extra'; +import { ParseError, parse as parseJsonc } from 'jsonc-parser'; + +export interface JsoncParseOptions { + allowTrailingComma?: boolean; + disallowComments?: boolean; + allowEmptyContent?: boolean; +} + +export class JsoncParseError extends Error { + constructor( + message: string, + public readonly parseErrors: ParseError[], + public readonly filePath?: string, + ) { + super(message); + this.name = 'JsoncParseError'; + } +} + +export const parseJsoncString = ( + content: string, + options: JsoncParseOptions = {}, +): any => { + const parseErrors: ParseError[] = []; + + const result = parseJsonc(content, parseErrors, { + allowTrailingComma: options.allowTrailingComma ?? true, + disallowComments: options.disallowComments ?? false, + allowEmptyContent: options.allowEmptyContent ?? false, + }); + + if (parseErrors.length > 0) { + const errorMessages = parseErrors.map( + (error) => `Line ${error.offset}: ${error.error}`, + ); + throw new JsoncParseError( + `JSONC parse errors:\n${errorMessages.join('\n')}`, + parseErrors, + ); + } + + return result; +}; + +export const parseJsoncFile = async ( + filePath: string, + options: JsoncParseOptions = {}, +): Promise => { + try { + const content = await fs.readFile(filePath, 'utf8'); + return parseJsoncString(content, options); + } catch (error) { + if (error instanceof JsoncParseError) { + throw new JsoncParseError(error.message, error.parseErrors, filePath); + } + throw new Error(`Failed to read file ${filePath}: ${error}`); + } +}; + +export const writeJsoncFile = async ( + filePath: string, + data: any, + options: { spaces?: number } = {}, +): Promise => { + const content = JSON.stringify(data, null, options.spaces ?? 2); + await fs.writeFile(filePath, content, 'utf8'); +}; diff --git a/packages/twenty-cli/src/utils/schema-validator.ts b/packages/twenty-cli/src/utils/schema-validator.ts new file mode 100644 index 0000000000..322ff4b8a1 --- /dev/null +++ b/packages/twenty-cli/src/utils/schema-validator.ts @@ -0,0 +1,120 @@ +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +export class SchemaValidationError extends Error { + constructor( + message: string, + public readonly errors: any[], + public readonly filePath?: string, + ) { + super(message); + this.name = 'SchemaValidationError'; + } +} + +export class SchemaValidator { + private ajv: Ajv; + private schemasLoaded = false; + + constructor() { + this.ajv = new Ajv({ + allErrors: true, + verbose: true, + strict: false, + }); + addFormats(this.ajv); + } + + private async loadSchemas(): Promise { + if (this.schemasLoaded) return; + + const schemasDir = path.join(__dirname, '../../schemas'); + + try { + // Load agent schema + const agentSchemaPath = path.join(schemasDir, 'agent.schema.json'); + const agentSchema = await fs.readJson(agentSchemaPath); + this.ajv.addSchema(agentSchema, 'agent'); + + // Load app manifest schema + const appSchemaPath = path.join(schemasDir, 'app-manifest.schema.json'); + const appSchema = await fs.readJson(appSchemaPath); + this.ajv.addSchema(appSchema, 'app-manifest'); + + this.schemasLoaded = true; + } catch { + // Gracefully handle missing schemas in development + console.warn('Warning: Could not load JSON schemas for validation'); + this.schemasLoaded = true; // Prevent retry + } + } + + async validateAgent(agent: any, filePath?: string): Promise { + await this.loadSchemas(); + + const validate = this.ajv.getSchema('agent'); + if (!validate) { + // Schema not available, skip validation + return; + } + + const valid = validate(agent); + if (!valid) { + const errorMessages = this.formatErrors(validate.errors || []); + throw new SchemaValidationError( + `Agent validation failed:\n${errorMessages}`, + validate.errors || [], + filePath, + ); + } + } + + async validateAppManifest(manifest: any, filePath?: string): Promise { + await this.loadSchemas(); + + const validate = this.ajv.getSchema('app-manifest'); + if (!validate) { + // Schema not available, skip validation + return; + } + + const valid = validate(manifest); + if (!valid) { + const errorMessages = this.formatErrors(validate.errors || []); + throw new SchemaValidationError( + `App manifest validation failed:\n${errorMessages}`, + validate.errors || [], + filePath, + ); + } + } + + private formatErrors(errors: any[]): string { + return errors + .map((error) => { + const path = error.instancePath || 'root'; + const message = error.message; + const value = + error.data !== undefined + ? ` (got: ${JSON.stringify(error.data)})` + : ''; + return ` β€’ ${path}: ${message}${value}`; + }) + .join('\n'); + } + + // Get schema URLs for $schema references + static getSchemaUrls() { + return { + agent: + 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/agent.schema.json', + appManifest: + 'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/app-manifest.schema.json', + }; + } +} + +// Singleton instance +export const schemaValidator = new SchemaValidator(); diff --git a/packages/twenty-cli/tsconfig.json b/packages/twenty-cli/tsconfig.json new file mode 100644 index 0000000000..9f0828f25c --- /dev/null +++ b/packages/twenty-cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "CommonJS", + "target": "ES2022", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 53fecc4c44..411fcce25d 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -171,6 +171,20 @@ export type AppTokenEdge = { node: AppToken; }; +export type Application = { + __typename?: 'Application'; + createdAt: Scalars['DateTime']; + description?: Maybe; + id: Scalars['UUID']; + label: Scalars['String']; + sourcePath: Scalars['String']; + sourceType: Scalars['String']; + standardId?: Maybe; + updatedAt: Scalars['DateTime']; + version?: Maybe; + workspaceId: Scalars['UUID']; +}; + export type ApprovedAccessDomain = { __typename?: 'ApprovedAccessDomain'; createdAt: Scalars['DateTime']; @@ -1498,6 +1512,7 @@ export type Mutation = { submitFormStep: Scalars['Boolean']; switchToEnterprisePlan: BillingUpdateOutput; switchToYearlyInterval: BillingUpdateOutput; + syncApplication: Application; syncRemoteTable: RemoteTable; syncRemoteTableSchemaChanges: RemoteTable; trackAnalytics: Analytics; @@ -2085,6 +2100,11 @@ export type MutationSubmitFormStepArgs = { }; +export type MutationSyncApplicationArgs = { + manifest: Scalars['JSON']; +}; + + export type MutationSyncRemoteTableArgs = { input: RemoteTableInput; }; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index b511f0cb39..11eaa4f554 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -171,6 +171,20 @@ export type AppTokenEdge = { node: AppToken; }; +export type Application = { + __typename?: 'Application'; + createdAt: Scalars['DateTime']; + description?: Maybe; + id: Scalars['UUID']; + label: Scalars['String']; + sourcePath: Scalars['String']; + sourceType: Scalars['String']; + standardId?: Maybe; + updatedAt: Scalars['DateTime']; + version?: Maybe; + workspaceId: Scalars['UUID']; +}; + export type ApprovedAccessDomain = { __typename?: 'ApprovedAccessDomain'; createdAt: Scalars['DateTime']; @@ -1453,6 +1467,7 @@ export type Mutation = { submitFormStep: Scalars['Boolean']; switchToEnterprisePlan: BillingUpdateOutput; switchToYearlyInterval: BillingUpdateOutput; + syncApplication: Application; trackAnalytics: Analytics; updateApiKey?: Maybe; updateCoreView: CoreView; @@ -2016,6 +2031,11 @@ export type MutationSubmitFormStepArgs = { }; +export type MutationSyncApplicationArgs = { + manifest: Scalars['JSON']; +}; + + export type MutationTrackAnalyticsArgs = { event?: InputMaybe; name?: InputMaybe; diff --git a/packages/twenty-server/@types/jest.d.ts b/packages/twenty-server/@types/jest.d.ts index 188df37b77..6f982c1159 100644 --- a/packages/twenty-server/@types/jest.d.ts +++ b/packages/twenty-server/@types/jest.d.ts @@ -1,6 +1,11 @@ +import { type INestApplication } from '@nestjs/common'; + import 'jest'; import { type DataSource } from 'typeorm'; +import { type DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command'; +import { type DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; + declare module '@jest/types' { namespace Config { interface ConfigGlobals { @@ -27,7 +32,12 @@ declare global { const API_KEY_ACCESS_TOKEN: string; const ACME_JONY_MEMBER_ACCESS_TOKEN: string; const WORKSPACE_AGNOSTIC_TOKEN: string; - const testDataSource: DataSource; + + // Additional global properties set during test setup + var testDataSource: DataSource; + var app: INestApplication; + var dataSourceService: DataSourceService; + var dataSeedWorkspaceCommand: DataSeedWorkspaceCommand; } export {}; diff --git a/packages/twenty-server/eslint.config.mjs b/packages/twenty-server/eslint.config.mjs index 9fba3b8e2e..17acbcb0fc 100644 --- a/packages/twenty-server/eslint.config.mjs +++ b/packages/twenty-server/eslint.config.mjs @@ -29,6 +29,7 @@ export default [ 'packages/twenty-server/node_modules/**', 'packages/twenty-server/dist/**', '**/node_modules/**', + '**/.local-storage/**', 'src/engine/workspace-manager/dev-seeder/data/constants/**', 'src/engine/workspace-manager/dev-seeder/data/seeds/**', 'src/utils/email-providers.ts', diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 6a70c7f1b0..47d71fbae8 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -149,7 +149,7 @@ "monaco-editor": "^0.51.0", "monaco-editor-auto-typings": "^0.4.5", "ms": "2.1.3", - "nest-commander": "3.14.0", + "nest-commander": "^3.19.1", "node-ical": "^0.20.1", "nodemailer": "6.9.14", "openapi-types": "12.1.3", diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1757491357122-addApplicationEntityAndRelationships.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1757491357122-addApplicationEntityAndRelationships.ts new file mode 100644 index 0000000000..13c3053ab1 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1757491357122-addApplicationEntityAndRelationships.ts @@ -0,0 +1,79 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddApplicationEntityAndRelationships1757491357122 + implements MigrationInterface +{ + name = 'AddApplicationEntityAndRelationships1757491357122'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "core"."application" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "standardId" uuid, + "label" text NOT NULL, + "description" text, + "version" text, + "sourceType" text NOT NULL DEFAULT 'local', + "sourcePath" text NOT NULL, + "workspaceId" uuid NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "deletedAt" TIMESTAMP WITH TIME ZONE, + CONSTRAINT "PK_569e0c3e863ebdf5f2408ee1670" PRIMARY KEY ("id") + ) + `); + + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "IDX_APPLICATION_WORKSPACE_ID" ON "core"."application" ("workspaceId") + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "IDX_APPLICATION_STANDARD_ID_WORKSPACE_ID_UNIQUE" + ON "core"."application" ("standardId", "workspaceId") + WHERE "deletedAt" IS NULL AND "standardId" IS NOT NULL + `); + + await queryRunner.query(` + ALTER TABLE "core"."application" + ADD CONSTRAINT "FK_08d1d5e33c2a3ce7c140e9b335b" + FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") + ON DELETE CASCADE ON UPDATE NO ACTION + `); + + await queryRunner.query(` + ALTER TABLE "core"."agent" + ADD COLUMN IF NOT EXISTS "applicationId" uuid + `); + + await queryRunner.query(` + ALTER TABLE "core"."agent" + ADD CONSTRAINT "FK_259c48f99f625708723414adb5d" + FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") + ON DELETE SET NULL ON UPDATE NO ACTION + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "core"."agent" DROP CONSTRAINT IF EXISTS "FK_259c48f99f625708723414adb5d" + `); + + await queryRunner.query(` + ALTER TABLE "core"."application" DROP CONSTRAINT IF EXISTS "FK_08d1d5e33c2a3ce7c140e9b335b" + `); + + await queryRunner.query(` + DROP INDEX IF EXISTS "core"."IDX_APPLICATION_STANDARD_ID_WORKSPACE_ID_UNIQUE" + `); + + await queryRunner.query(` + DROP INDEX IF EXISTS "core"."IDX_APPLICATION_WORKSPACE_ID" + `); + + await queryRunner.query(` + ALTER TABLE "core"."agent" DROP COLUMN IF EXISTS "applicationId" + `); + + await queryRunner.query(`DROP TABLE IF EXISTS "core"."application"`); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application.entity.ts b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts new file mode 100644 index 0000000000..567c2e7965 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application.entity.ts @@ -0,0 +1,65 @@ +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Entity({ name: 'application', schema: 'core' }) +@Index('IDX_APPLICATION_WORKSPACE_ID', ['workspaceId']) +@Index( + 'IDX_APPLICATION_STANDARD_ID_WORKSPACE_ID_UNIQUE', + ['standardId', 'workspaceId'], + { + unique: true, + where: '"deletedAt" IS NULL AND "standardId" IS NOT NULL', + }, +) +export class ApplicationEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true, type: 'uuid' }) + standardId?: string; + + @Column({ nullable: false, type: 'text' }) + label: string; + + @Column({ nullable: true, type: 'text' }) + description: string | null; + + @Column({ nullable: true, type: 'text' }) + version: string | null; + + @Column({ type: 'text', default: 'local' }) + sourceType: 'local'; + + @Column({ nullable: false, type: 'text' }) + sourcePath: string; + + @Column({ nullable: false, type: 'uuid' }) + workspaceId: string; + + @ManyToOne(() => Workspace, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt: Date | null; +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application.module.ts b/packages/twenty-server/src/engine/core-modules/application/application.module.ts new file mode 100644 index 0000000000..341759fb33 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { LocalApplicationSourceProvider } from 'src/engine/core-modules/application/providers/local-application-source.provider'; +import { ApplicationSyncAgentService } from 'src/engine/core-modules/application/services/application-sync-agent.service'; +import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ApplicationEntity, AgentEntity, Workspace]), + ], + providers: [ + ApplicationResolver, + ApplicationService, + ApplicationSyncService, + ApplicationSyncAgentService, + LocalApplicationSourceProvider, + ], + exports: [ApplicationService, ApplicationSyncService], +}) +export class ApplicationModule {} diff --git a/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts new file mode 100644 index 0000000000..ab70edd30d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application.resolver.ts @@ -0,0 +1,35 @@ +import { UseGuards } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; + +import GraphQLJSON from 'graphql-type-json'; + +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +import { ApplicationDTO } from './dtos/application.dto'; +import { ApplicationSyncService } from './services/application-sync.service'; +import { ApplicationManifest } from './types/application-manifest.type'; + +@UseGuards(WorkspaceAuthGuard) +@Resolver() +export class ApplicationResolver { + constructor( + private readonly applicationSyncService: ApplicationSyncService, + ) {} + + @Mutation(() => ApplicationDTO) + async syncApplication( + @Args('manifest', { type: () => GraphQLJSON }) + manifest: ApplicationManifest, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + const application = + await this.applicationSyncService.synchronizeFromManifest( + workspaceId, + manifest, + ); + + return application; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts new file mode 100644 index 0000000000..0dcc5de42d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; + +@Injectable() +export class ApplicationService { + constructor( + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + ) {} + + async findById(id: string): Promise { + return this.applicationRepository.findOne({ + where: { id }, + }); + } + + async findByStandardId( + standardId: string, + workspaceId: string, + ): Promise { + return this.applicationRepository.find({ + where: { + standardId, + workspaceId, + }, + }); + } + + async create(data: { + standardId?: string; + label: string; + description?: string; + version?: string; + sourcePath: string; + workspaceId: string; + }): Promise { + const application = this.applicationRepository.create({ + ...data, + sourceType: 'local', + }); + + return this.applicationRepository.save(application); + } + + async update( + id: string, + data: { + label?: string; + description?: string; + version?: string; + sourcePath?: string; + }, + ): Promise { + await this.applicationRepository.update({ id }, data); + + const updatedApplication = await this.findById(id); + + if (!updatedApplication) { + throw new Error(`Failed to update application with id ${id}`); + } + + return updatedApplication; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts new file mode 100644 index 0000000000..44447c62ff --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/dtos/application.dto.ts @@ -0,0 +1,58 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { + IsDateString, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; + +@ObjectType('Application') +export class ApplicationDTO { + @IsUUID() + @IsNotEmpty() + @Field(() => UUIDScalarType) + id: string; + + @IsOptional() + @IsUUID() + @Field(() => UUIDScalarType, { nullable: true }) + standardId?: string; + + @IsString() + @Field(() => String) + label: string; + + @IsOptional() + @IsString() + @Field(() => String, { nullable: true }) + description?: string | null; + + @IsOptional() + @IsString() + @Field(() => String, { nullable: true }) + version?: string | null; + + @IsString() + @Field(() => String) + sourceType: string; + + @IsString() + @Field(() => String) + sourcePath: string; + + @IsUUID() + @Field(() => UUIDScalarType) + workspaceId: string; + + @IsDateString() + @Field(() => Date) + createdAt: Date; + + @IsDateString() + @Field(() => Date) + updatedAt: Date; +} diff --git a/packages/twenty-server/src/engine/core-modules/application/providers/local-application-source.provider.ts b/packages/twenty-server/src/engine/core-modules/application/providers/local-application-source.provider.ts new file mode 100644 index 0000000000..93018cc8a1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/providers/local-application-source.provider.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { promises as fs } from 'fs'; +import * as path from 'path'; + +import { ApplicationManifest } from 'src/engine/core-modules/application/types/application-manifest.type'; + +@Injectable() +export class LocalApplicationSourceProvider { + async fetchManifest(localPath: string): Promise { + const manifestPath = path.join(localPath, 'twenty-app.json'); + + try { + const manifestContent = await fs.readFile(manifestPath, 'utf-8'); + + return JSON.parse(manifestContent) as ApplicationManifest; + } catch (error) { + throw new Error( + `Failed to read manifest from ${manifestPath}: ${error.message}`, + ); + } + } + + async validateSource(localPath: string): Promise { + const manifestPath = path.join(localPath, 'twenty-app.json'); + + try { + await fs.access(manifestPath); + + return true; + } catch { + return false; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-sync-agent.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-sync-agent.service.ts new file mode 100644 index 0000000000..a3edd9eb0a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-sync-agent.service.ts @@ -0,0 +1,72 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ApplicationSyncContext } from 'src/engine/core-modules/application/services/application-sync.service'; +import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; +import { FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; + +@Injectable() +export class ApplicationSyncAgentService { + private readonly logger = new Logger(ApplicationSyncAgentService.name); + + constructor( + @InjectRepository(AgentEntity) + private readonly agentRepository: Repository, + ) {} + + async synchronize( + context: ApplicationSyncContext, + agents: FlatAgent[], + ): Promise { + if (!agents || agents.length === 0) { + this.logger.log('No agents to synchronize'); + + return; + } + + for (const agentDefinition of agents) { + this.logger.log(`Syncing agent: ${agentDefinition.label}`); + + // Check if agent already exists + const existingAgent = await this.agentRepository.findOne({ + where: { + workspaceId: context.workspaceId, + applicationId: context.applicationId, + name: agentDefinition.name, + }, + }); + + if (existingAgent) { + // Update existing agent + await this.agentRepository.update( + { id: existingAgent.id }, + { + label: agentDefinition.label, + description: agentDefinition.description, + icon: agentDefinition.icon, + prompt: agentDefinition.prompt, + modelId: agentDefinition.modelId, + }, + ); + this.logger.log(`Updated agent: ${agentDefinition.label}`); + } else { + // Create new agent + const newAgent = this.agentRepository.create({ + name: agentDefinition.name, + label: agentDefinition.label, + description: agentDefinition.description, + icon: agentDefinition.icon, + prompt: agentDefinition.prompt, + modelId: agentDefinition.modelId, + workspaceId: context.workspaceId, + applicationId: context.applicationId, + }); + + await this.agentRepository.save(newAgent); + this.logger.log(`Created agent: ${agentDefinition.label}`); + } + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts new file mode 100644 index 0000000000..3243b5c05c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/services/application-sync.service.ts @@ -0,0 +1,120 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { LocalApplicationSourceProvider } from 'src/engine/core-modules/application/providers/local-application-source.provider'; +import { ApplicationSyncAgentService } from 'src/engine/core-modules/application/services/application-sync-agent.service'; +import { FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; + +export interface ApplicationSyncContext { + workspaceId: string; + featureFlags: Record; + applicationId: string; +} + +interface ApplicationManifest { + standardId: string; + label: string; + description?: string; + version?: string; + agents?: FlatAgent[]; +} + +@Injectable() +export class ApplicationSyncService { + private readonly logger = new Logger(ApplicationSyncService.name); + + constructor( + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + private readonly localSourceProvider: LocalApplicationSourceProvider, + private readonly applicationSyncAgentService: ApplicationSyncAgentService, + private readonly applicationService: ApplicationService, + ) {} + + public async synchronize(context: ApplicationSyncContext): Promise { + this.logger.log(`Syncing agents for application: ${context.applicationId}`); + + const application = await this.applicationRepository.findOne({ + where: { id: context.applicationId }, + }); + + if (!application) { + throw new Error(`Application with ID ${context.applicationId} not found`); + } + + const manifest = await this.localSourceProvider.fetchManifest( + application.sourcePath, + ); + + this.logger.log(`Syncing application: ${manifest.label}`); + + await this.applicationSyncAgentService.synchronize( + context, + manifest.agents, + ); + + this.logger.log('βœ… Agent sync completed'); + } + + public async synchronizeFromManifest( + workspaceId: string, + manifest: ApplicationManifest, + ): Promise { + this.logger.log(`Syncing application from manifest: ${manifest.label}`); + + // Find or create application + let application = await this.applicationService.findByStandardId( + manifest.standardId, + workspaceId, + ); + + if (application.length === 0) { + // Create new application + application = [ + await this.applicationService.create({ + standardId: manifest.standardId, + label: manifest.label, + description: manifest.description, + version: manifest.version, + sourcePath: 'cli-sync', // Placeholder for CLI-synced apps + workspaceId, + }), + ]; + this.logger.log(`Created new application: ${manifest.label}`); + } else { + // Update existing application + const existingApp = application[0]; + + await this.applicationService.update(existingApp.id, { + label: manifest.label, + description: manifest.description, + version: manifest.version, + }); + this.logger.log(`Updated existing application: ${manifest.label}`); + } + + const app = application[0]; + + // Sync agents + if (manifest.agents && manifest.agents.length > 0) { + const context: ApplicationSyncContext = { + workspaceId, + featureFlags: {}, // TODO: Get actual feature flags + applicationId: app.id, + }; + + await this.applicationSyncAgentService.synchronize( + context, + manifest.agents, + ); + } + + this.logger.log('βœ… Application sync from manifest completed'); + + return app; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/types/application-manifest.type.ts b/packages/twenty-server/src/engine/core-modules/application/types/application-manifest.type.ts new file mode 100644 index 0000000000..3a634f012f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/types/application-manifest.type.ts @@ -0,0 +1,10 @@ +import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; + +export type ApplicationManifest = { + standardId: string; + label: string; + description?: string; + icon?: string; + version: string; + agents: FlatAgent[]; +}; diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index f22c778a5f..c796a42bdf 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -8,6 +8,7 @@ import { AdminPanelModule } from 'src/engine/core-modules/admin-panel/admin-pane import { AiModule } from 'src/engine/core-modules/ai/ai.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingWebhookModule } from 'src/engine/core-modules/billing-webhook/billing-webhook.module'; @@ -69,6 +70,7 @@ import { FileModule } from './file/file.module'; FeatureFlagModule, FileModule, OpenApiModule, + ApplicationModule, AppTokenModule, TimelineMessagingModule, TimelineCalendarEventModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent.entity.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent.entity.ts index 9ab08518bd..37e5646a03 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent.entity.ts @@ -14,6 +14,7 @@ import { import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentChatThreadEntity } from './agent-chat-thread.entity'; @@ -56,6 +57,9 @@ export class AgentEntity { @Column({ nullable: false, type: 'uuid' }) workspaceId: string; + @Column({ nullable: true, type: 'uuid' }) + applicationId: string | null; + @Column({ default: false }) isCustom: boolean; @@ -65,6 +69,13 @@ export class AgentEntity { @JoinColumn({ name: 'workspaceId' }) workspace: Relation; + @ManyToOne(() => ApplicationEntity, { + onDelete: 'SET NULL', + nullable: true, + }) + @JoinColumn({ name: 'applicationId' }) + application: Relation | null; + @OneToMany(() => AgentChatThreadEntity, (chatThread) => chatThread.agent) chatThreads: Relation; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/types/flat-agent.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/types/flat-agent.type.ts index ecffde2df4..696153cfaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/types/flat-agent.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/types/flat-agent.type.ts @@ -5,6 +5,7 @@ export const agentEntityRelationProperties = [ 'chatThreads', 'outgoingHandoffs', 'incomingHandoffs', + 'application', ] as const; export type AgentEntityRelationProperties = diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts index 3dba0594af..f9c3c34f4d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util.ts @@ -17,5 +17,6 @@ export const transformAgentEntityToFlatAgent = ( workspaceId: agentEntity.workspaceId, isCustom: agentEntity.isCustom, universalIdentifier: agentEntity.standardId || agentEntity.id, + applicationId: agentEntity.applicationId, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent.ts index ed6d61cbc4..a331ecaf3a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent.ts @@ -7,6 +7,7 @@ export const WORKFLOW_CREATION_AGENT: StandardAgentDefinition = { label: 'Workflow Creation Agent', description: 'AI agent specialized in creating and managing workflows', icon: 'IconSettingsAutomation', + applicationId: null, prompt: `You are a Workflow Creation Agent specialized in helping users create, modify, and manage workflows in Twenty. Your capabilities include: diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index 4f7b266fd9..dd1dbd51c4 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -177,6 +177,8 @@ export const createAgentToolTestModule = label: 'Test Agent', icon: 'IconTest', isCustom: false, + applicationId: null, + application: null, description: 'Test agent for integration tests', prompt: 'You are a test agent', modelId: 'gpt-4o', diff --git a/packages/twenty-server/test/integration/utils/delete-all-records.ts b/packages/twenty-server/test/integration/utils/delete-all-records.ts index 2c54944278..f03a1670e3 100644 --- a/packages/twenty-server/test/integration/utils/delete-all-records.ts +++ b/packages/twenty-server/test/integration/utils/delete-all-records.ts @@ -2,7 +2,6 @@ const TEST_SCHEMA_NAME = 'workspace_1wgvd1injqtife6y4rvfbu3h5'; export const deleteAllRecords = async (objectNameSingular: string) => { try { - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query( `DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}"`, ); diff --git a/packages/twenty-server/test/integration/utils/delete-records-by-ids.ts b/packages/twenty-server/test/integration/utils/delete-records-by-ids.ts index 786a916d3c..4d1bcdbe38 100644 --- a/packages/twenty-server/test/integration/utils/delete-records-by-ids.ts +++ b/packages/twenty-server/test/integration/utils/delete-records-by-ids.ts @@ -14,7 +14,6 @@ export const deleteRecordsByIds = async ( .map((_, index) => `$${index + 1}`) .join(', '); - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query( `DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}" WHERE id IN (${placeholders})`, recordIds, diff --git a/packages/twenty-server/test/integration/utils/page-layout-tab-test.util.ts b/packages/twenty-server/test/integration/utils/page-layout-tab-test.util.ts index 37422f7012..d99395f38a 100644 --- a/packages/twenty-server/test/integration/utils/page-layout-tab-test.util.ts +++ b/packages/twenty-server/test/integration/utils/page-layout-tab-test.util.ts @@ -1,7 +1,6 @@ import { type PageLayoutTabEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-tab.entity'; export const cleanupPageLayoutTabRecords = async (): Promise => { - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query(`DELETE from "core"."pageLayoutTab"`); }; diff --git a/packages/twenty-server/test/integration/utils/page-layout-test.util.ts b/packages/twenty-server/test/integration/utils/page-layout-test.util.ts index 099f341ef4..9a4490204e 100644 --- a/packages/twenty-server/test/integration/utils/page-layout-test.util.ts +++ b/packages/twenty-server/test/integration/utils/page-layout-test.util.ts @@ -2,7 +2,6 @@ import { type PageLayoutEntity } from 'src/engine/core-modules/page-layout/entit import { PageLayoutType } from 'src/engine/core-modules/page-layout/enums/page-layout-type.enum'; export const cleanupPageLayoutRecords = async (): Promise => { - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query(`DELETE from "core"."pageLayout"`); }; diff --git a/packages/twenty-server/test/integration/utils/page-layout-widget-test.util.ts b/packages/twenty-server/test/integration/utils/page-layout-widget-test.util.ts index e86d9572b6..e67a945687 100644 --- a/packages/twenty-server/test/integration/utils/page-layout-widget-test.util.ts +++ b/packages/twenty-server/test/integration/utils/page-layout-widget-test.util.ts @@ -1,7 +1,6 @@ import { type PageLayoutWidgetEntity } from 'src/engine/core-modules/page-layout/entities/page-layout-widget.entity'; export const cleanupPageLayoutWidgetRecords = async (): Promise => { - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query(`DELETE from "core"."pageLayoutWidget"`); }; diff --git a/packages/twenty-server/test/integration/utils/setup-test.ts b/packages/twenty-server/test/integration/utils/setup-test.ts index e717eabfab..c377bf67ef 100644 --- a/packages/twenty-server/test/integration/utils/setup-test.ts +++ b/packages/twenty-server/test/integration/utils/setup-test.ts @@ -7,8 +7,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { createApp } from './create-app'; -// @ts-expect-error legacy noImplicitAny -export default async (_, projectConfig: JestConfigWithTsJest) => { +export default async (_: unknown, projectConfig: JestConfigWithTsJest) => { const app = await createApp({}); if (!projectConfig.globals) { @@ -17,14 +16,10 @@ export default async (_, projectConfig: JestConfigWithTsJest) => { await rawDataSource.initialize(); - await app.listen(projectConfig.globals.APP_PORT); + await app.listen(projectConfig.globals.APP_PORT as number); - // @ts-expect-error legacy noImplicitAny global.app = app; - // @ts-expect-error legacy noImplicitAny global.testDataSource = rawDataSource; - // @ts-expect-error legacy noImplicitAny global.dataSourceService = app.get(DataSourceService); - // @ts-expect-error legacy noImplicitAny global.dataSeedWorkspaceCommand = app.get(DataSeedWorkspaceCommand); }; diff --git a/packages/twenty-server/test/integration/utils/teardown-test.ts b/packages/twenty-server/test/integration/utils/teardown-test.ts index ffc5c16c42..72549ea134 100644 --- a/packages/twenty-server/test/integration/utils/teardown-test.ts +++ b/packages/twenty-server/test/integration/utils/teardown-test.ts @@ -1,8 +1,6 @@ import 'tsconfig-paths/register'; export default async () => { - // @ts-expect-error legacy noImplicitAny global.testDataSource.destroy(); - // @ts-expect-error legacy noImplicitAny global.app.close(); }; diff --git a/packages/twenty-server/test/integration/utils/view-test.util.ts b/packages/twenty-server/test/integration/utils/view-test.util.ts index 9c0c6339be..7b96cfb78f 100644 --- a/packages/twenty-server/test/integration/utils/view-test.util.ts +++ b/packages/twenty-server/test/integration/utils/view-test.util.ts @@ -7,7 +7,6 @@ import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.enti import { ViewFilterGroupLogicalOperator } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity'; export const cleanupViewRecords = async (): Promise => { - // @ts-expect-error legacy noImplicitAny await global.testDataSource.query(`DELETE from "core"."view"`); }; diff --git a/yarn.lock b/yarn.lock index 530b2db1f1..a884214d98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5388,15 +5388,15 @@ __metadata: languageName: node linkType: hard -"@golevelup/nestjs-discovery@npm:4.0.1": - version: 4.0.1 - resolution: "@golevelup/nestjs-discovery@npm:4.0.1" +"@golevelup/nestjs-discovery@npm:4.0.3": + version: 4.0.3 + resolution: "@golevelup/nestjs-discovery@npm:4.0.3" dependencies: lodash: "npm:^4.17.21" peerDependencies: - "@nestjs/common": ^10.x - "@nestjs/core": ^10.x - checksum: 10c0/232de5f8f051eab1ed0a4b54e3cbb1532ce0311ed7a3abaa1f2e86b6189ecf17b8b01c8faff318d058cbed3daf6bc93c50bf64a30d382581e8176555619c12c8 + "@nestjs/common": ^10.x || ^11.0.0 + "@nestjs/core": ^10.x || ^11.0.0 + checksum: 10c0/e5b95a5d8a25a5ca98ea50f66cb71870ab075f25080782ea2a2c550e17a1c61148b0c18f6b013eec3df225afe6989531cb9f6bb7f289028bb446e6dc867a5b0f languageName: node linkType: hard @@ -6824,50 +6824,193 @@ __metadata: languageName: node linkType: hard -"@inquirer/confirm@npm:^3.0.0": - version: 3.1.22 - resolution: "@inquirer/confirm@npm:3.1.22" +"@inquirer/checkbox@npm:^2.5.0": + version: 2.5.0 + resolution: "@inquirer/checkbox@npm:2.5.0" dependencies: - "@inquirer/core": "npm:^9.0.10" - "@inquirer/type": "npm:^1.5.2" - checksum: 10c0/99e1a17e62a674d8e440a11bf4e9d5a62666247823b091314e52ee40929a6a6e8ce60086ec653bbeb59117bfc940d807c6f4b604cf5cf51f24009b9d09d5bf98 + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/679d17ffe3aef0825593f3bc8d193b6c37b860c6cf6e0e9a10d4e60cc254a2dfc5da4a982bf5b9b5147018e456fffcb0b0dadf93ee1914b9d600b0c814284e22 languageName: node linkType: hard -"@inquirer/core@npm:^9.0.10": - version: 9.0.10 - resolution: "@inquirer/core@npm:9.0.10" +"@inquirer/confirm@npm:^3.0.0, @inquirer/confirm@npm:^3.2.0": + version: 3.2.0 + resolution: "@inquirer/confirm@npm:3.2.0" dependencies: - "@inquirer/figures": "npm:^1.0.5" - "@inquirer/type": "npm:^1.5.2" + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/a2cbfc8ae9c880bba4cce1993f5c399fb0d12741fdd574917c87fceb40ece62ffa60e35aaadf4e62d7c114f54008e45aee5d6d90497bb62d493996c02725d243 + languageName: node + linkType: hard + +"@inquirer/core@npm:^9.1.0": + version: 9.2.1 + resolution: "@inquirer/core@npm:9.2.1" + dependencies: + "@inquirer/figures": "npm:^1.0.6" + "@inquirer/type": "npm:^2.0.0" "@types/mute-stream": "npm:^0.0.4" - "@types/node": "npm:^22.1.0" + "@types/node": "npm:^22.5.5" "@types/wrap-ansi": "npm:^3.0.0" ansi-escapes: "npm:^4.3.2" - cli-spinners: "npm:^2.9.2" cli-width: "npm:^4.1.0" mute-stream: "npm:^1.0.0" signal-exit: "npm:^4.1.0" strip-ansi: "npm:^6.0.1" wrap-ansi: "npm:^6.2.0" yoctocolors-cjs: "npm:^2.1.2" - checksum: 10c0/117f50a55b5ebee8bfc62ea6adec87035f28ee7ace1efea67895c3d32ab50bf569ecd3ca33c457d0c7ae4240b9fe4d7b698ab70946ac561ab579d0920ddc98bb + checksum: 10c0/11c14be77a9fa85831de799a585721b0a49ab2f3b7d8fd1780c48ea2b29229c6bdc94e7892419086d0f7734136c2ba87b6a32e0782571eae5bbd655b1afad453 languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.5": - version: 1.0.5 - resolution: "@inquirer/figures@npm:1.0.5" - checksum: 10c0/ec9ba23db42cb33fa18eb919abf2a18e750e739e64c1883ce4a98345cd5711c60cac12d1faf56a859f52d387deb221c8d3dfe60344ee07955a9a262f8b821fe3 +"@inquirer/editor@npm:^2.2.0": + version: 2.2.0 + resolution: "@inquirer/editor@npm:2.2.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + external-editor: "npm:^3.1.0" + checksum: 10c0/b8afc0790a7a5d82998bdfe469cbaa83b0cd0700be432cf95256c548e2a6a494997b5e93d65cbf94979c17b510758cf8494d85559f6b9508eb15d239a7f22aee languageName: node linkType: hard -"@inquirer/type@npm:^1.5.2": - version: 1.5.2 - resolution: "@inquirer/type@npm:1.5.2" +"@inquirer/expand@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/expand@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/f2030cb482a715e4d5153c19b3f0fd8bf47c16cdc16e1c669e90985386edf4f7b0f3b0e97e2990bb228878b93716228eb067d94fc557c25d3c5ee58747c0a995 + languageName: node + linkType: hard + +"@inquirer/external-editor@npm:^1.0.0": + version: 1.0.1 + resolution: "@inquirer/external-editor@npm:1.0.1" + dependencies: + chardet: "npm:^2.1.0" + iconv-lite: "npm:^0.6.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10c0/bdac4395e0bba7065d39b141d618bfc06369f246c402c511396a5238baf2657f3038ccba8438521a49e5cb602f4302b9d1f46b52b647b27af2c9911720022118 + languageName: node + linkType: hard + +"@inquirer/figures@npm:^1.0.5, @inquirer/figures@npm:^1.0.6": + version: 1.0.13 + resolution: "@inquirer/figures@npm:1.0.13" + checksum: 10c0/23700a4a0627963af5f51ef4108c338ae77bdd90393164b3fdc79a378586e1f5531259882b7084c690167bf5a36e83033e45aca0321570ba810890abe111014f + languageName: node + linkType: hard + +"@inquirer/input@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/input@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/44c8cea38c9192f528cae556f38709135a00230132deab3b9bb9a925375fce0513fecf4e8c1df7c4319e1ed7aa31fb4dd2c4956c8bc9dd39af087aafff5b6f1f + languageName: node + linkType: hard + +"@inquirer/number@npm:^1.1.0": + version: 1.1.0 + resolution: "@inquirer/number@npm:1.1.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + checksum: 10c0/db472dab57c951c4a083b2a749ce58262b1efd9889e7603de6e9c3f9af7d8dce8fbdfa3859f65402d3587470e0397a076e5fb4ed775db33310f17a42c9faeb20 + languageName: node + linkType: hard + +"@inquirer/password@npm:^2.2.0": + version: 2.2.0 + resolution: "@inquirer/password@npm:2.2.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + checksum: 10c0/fa4b335164b2c9c3304d29a7214ef93bac8d3da6788146603ea3d0485b8d811151e49bf66cb0dcc729a9dc21406c3a8c2718c5beec572a91d07026d22842c13f + languageName: node + linkType: hard + +"@inquirer/prompts@npm:^5.5.0": + version: 5.5.0 + resolution: "@inquirer/prompts@npm:5.5.0" + dependencies: + "@inquirer/checkbox": "npm:^2.5.0" + "@inquirer/confirm": "npm:^3.2.0" + "@inquirer/editor": "npm:^2.2.0" + "@inquirer/expand": "npm:^2.3.0" + "@inquirer/input": "npm:^2.3.0" + "@inquirer/number": "npm:^1.1.0" + "@inquirer/password": "npm:^2.2.0" + "@inquirer/rawlist": "npm:^2.3.0" + "@inquirer/search": "npm:^1.1.0" + "@inquirer/select": "npm:^2.5.0" + checksum: 10c0/2d62b50ca761b2bd2d5759f48c03758f1af0665ac602c26ae1ae257ac512cf5c27fd82cde108ee0c6371ec39187adc6f45637f31ca79adf5bf96579f23e77143 + languageName: node + linkType: hard + +"@inquirer/rawlist@npm:^2.3.0": + version: 2.3.0 + resolution: "@inquirer/rawlist@npm:2.3.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/d49d5e12b7a54394c140b27c8d8748ba1ab855c67c01fa72b5a63810f12865df3bf4d5ae929f54fad77b5fc2f7431a332ae1e5fe4babb335380c28917002f364 + languageName: node + linkType: hard + +"@inquirer/search@npm:^1.1.0": + version: 1.1.0 + resolution: "@inquirer/search@npm:1.1.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/20d7e910266b9e3f0dc8eef8f3007f487e6149fa8421d293eaf7c11a1e35c3d82aa30af118b3a6e35eed1048a27d7d806f45722abb10005db5d099ea64b00b17 + languageName: node + linkType: hard + +"@inquirer/select@npm:^2.5.0": + version: 2.5.0 + resolution: "@inquirer/select@npm:2.5.0" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/figures": "npm:^1.0.5" + "@inquirer/type": "npm:^1.5.3" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/280fa700187ff29da0ad4bf32aa11db776261584ddf5cc1ceac5caebb242a4ac0c5944af522a2579d78b6ec7d6e8b1b9f6564872101abd8dcc69929b4e33fc4c + languageName: node + linkType: hard + +"@inquirer/type@npm:^1.5.3": + version: 1.5.5 + resolution: "@inquirer/type@npm:1.5.5" dependencies: mute-stream: "npm:^1.0.0" - checksum: 10c0/e2c91562c07440620bd805a60438b78c188d2727d86f396a68c480e4357469a72cd80bd2c158faa6b987671911566bd4fc12976f4bdda1a3441594e318c40058 + checksum: 10c0/4c41736c09ba9426b5a9e44993bdd54e8f532e791518802e33866f233a2a6126a25c1c82c19d1abbf1df627e57b1b957dd3f8318ea96073d8bfc32193943bcb3 + languageName: node + linkType: hard + +"@inquirer/type@npm:^2.0.0": + version: 2.0.0 + resolution: "@inquirer/type@npm:2.0.0" + dependencies: + mute-stream: "npm:^1.0.0" + checksum: 10c0/8c663d52beb2b89a896d3c3d5cc3d6d024fa149e565555bcb42fa640cbe23fba7ff2c51445342cef1fe6e46305e2d16c1590fa1d11ad0ddf93a67b655ef41f0a languageName: node linkType: hard @@ -6956,17 +7099,17 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/console@npm:30.0.5" +"@jest/console@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/console@npm:30.1.2" dependencies: "@jest/types": "npm:30.0.5" "@types/node": "npm:*" chalk: "npm:^4.1.2" - jest-message-util: "npm:30.0.5" + jest-message-util: "npm:30.1.0" jest-util: "npm:30.0.5" slash: "npm:^3.0.0" - checksum: 10c0/1400e9ee281dd070f543f8f8696b9aca4ba1f81d5cbfb3cae030664012ff5961c76ac2c8ccee172e416e15f88af3b10840548adbee4de0ec63100d44416b17ef + checksum: 10c0/108c8d891955f97ecfb8a687ce8e5ce2d0bcc59ef4b5400ae219fb5183f4b35a0dd1ba3429bbe9abe9a5134a8f8e1ce89f09f2e2758487028eb339406df58b05 languageName: node linkType: hard @@ -6984,6 +7127,47 @@ __metadata: languageName: node linkType: hard +"@jest/core@npm:30.1.3": + version: 30.1.3 + resolution: "@jest/core@npm:30.1.3" + dependencies: + "@jest/console": "npm:30.1.2" + "@jest/pattern": "npm:30.0.1" + "@jest/reporters": "npm:30.1.3" + "@jest/test-result": "npm:30.1.3" + "@jest/transform": "npm:30.1.2" + "@jest/types": "npm:30.0.5" + "@types/node": "npm:*" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + exit-x: "npm:^0.2.2" + graceful-fs: "npm:^4.2.11" + jest-changed-files: "npm:30.0.5" + jest-config: "npm:30.1.3" + jest-haste-map: "npm:30.1.0" + jest-message-util: "npm:30.1.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.1.3" + jest-resolve-dependencies: "npm:30.1.3" + jest-runner: "npm:30.1.3" + jest-runtime: "npm:30.1.3" + jest-snapshot: "npm:30.1.2" + jest-util: "npm:30.0.5" + jest-validate: "npm:30.1.0" + jest-watcher: "npm:30.1.3" + micromatch: "npm:^4.0.8" + pretty-format: "npm:30.0.5" + slash: "npm:^3.0.0" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/0f934a027b969daebe8e44ba3127a80c2ac1a65becc16b1f9d5a072718d950ce3c1910ce0675a98d5ef1777014e842b5bf6dd736fb9c6442a22ebfd326ae50c5 + languageName: node + linkType: hard + "@jest/core@npm:^29.7.0": version: 29.7.0 resolution: "@jest/core@npm:29.7.0" @@ -7074,15 +7258,15 @@ __metadata: languageName: node linkType: hard -"@jest/environment@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/environment@npm:30.0.5" +"@jest/environment@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/environment@npm:30.1.2" dependencies: - "@jest/fake-timers": "npm:30.0.5" + "@jest/fake-timers": "npm:30.1.2" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" jest-mock: "npm:30.0.5" - checksum: 10c0/e403b6f98fa3e39dd6462fa192e3bd55e9ac9c2322ca4471b9342495913a90ecaa5fc53238d4ad8a0dca7d53aa4b9de122721234e36f3a0445031c25757a3178 + checksum: 10c0/41ac75f75d37f76cf89d97df55da107972068e8e4d4fa230bef5119b0efcb8a9feaca405a776bcbe7207f9c162990d41894636c793d3a9f0b9708e7c8ef57c6e languageName: node linkType: hard @@ -7098,12 +7282,12 @@ __metadata: languageName: node linkType: hard -"@jest/expect-utils@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/expect-utils@npm:30.0.5" +"@jest/expect-utils@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/expect-utils@npm:30.1.2" dependencies: - "@jest/get-type": "npm:30.0.1" - checksum: 10c0/d0ee162a1d1816724580bea53e7b422b891af073bdae439e78d04d5db09e6557e334f4c3d2892b9de750a59e79605f55d3ca8dbec9fb2ba33d8b803ed98463ad + "@jest/get-type": "npm:30.1.0" + checksum: 10c0/5b6c4d400ad0bd22960bd77750baf55b24bf1ebdc2cec328afe275967db76bf94f797ca4c9817cdb86bc7820b9219d3f493705f3fa94fe7720960e47805a8e1b languageName: node linkType: hard @@ -7116,13 +7300,13 @@ __metadata: languageName: node linkType: hard -"@jest/expect@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/expect@npm:30.0.5" +"@jest/expect@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/expect@npm:30.1.2" dependencies: - expect: "npm:30.0.5" - jest-snapshot: "npm:30.0.5" - checksum: 10c0/6ff40adf2f2cfa53f7a23bc2b85ae99d3264420e81202d45d1dc198009f4441ee575d910e79e589f69c2dd47e0ef9a3b66018f44760da02d98f474361f7c4d1c + expect: "npm:30.1.2" + jest-snapshot: "npm:30.1.2" + checksum: 10c0/e441639d902f8a9e894e856b98378cf2b14b102c04df160203482087e06a1bdbb961beb5c021012946dfa72019594e7dd0456fb5a1572f1f4d8f16475b44b0b3 languageName: node linkType: hard @@ -7150,17 +7334,17 @@ __metadata: languageName: node linkType: hard -"@jest/fake-timers@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/fake-timers@npm:30.0.5" +"@jest/fake-timers@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/fake-timers@npm:30.1.2" dependencies: "@jest/types": "npm:30.0.5" "@sinonjs/fake-timers": "npm:^13.0.0" "@types/node": "npm:*" - jest-message-util: "npm:30.0.5" + jest-message-util: "npm:30.1.0" jest-mock: "npm:30.0.5" jest-util: "npm:30.0.5" - checksum: 10c0/4c403e624d758780016c2012b23112ff421efd601def289b201c4a5e03c46f995c7c3509d7b0b56dbe17cd5cbc66920734bd976ebe12125d6fd864d71888a50d + checksum: 10c0/043ac3b5d60041550d86be9f178caf6146a579fb7a6b10b497e9f8c46a9198c1c5f4a8a2177cd5a128e1fa4ba252dfcaa13b1975ef180fa941551efe126f4b61 languageName: node linkType: hard @@ -7178,22 +7362,22 @@ __metadata: languageName: node linkType: hard -"@jest/get-type@npm:30.0.1": - version: 30.0.1 - resolution: "@jest/get-type@npm:30.0.1" - checksum: 10c0/92437ae42d0df57e8acc2d067288151439db4752cde4f5e680c73c8a6e34568bbd8c1c81a2f2f9a637a619c2aac8bc87553fb80e31475b59e2ed789a71e5e540 +"@jest/get-type@npm:30.1.0": + version: 30.1.0 + resolution: "@jest/get-type@npm:30.1.0" + checksum: 10c0/3e65fd5015f551c51ec68fca31bbd25b466be0e8ee8075d9610fa1c686ea1e70a942a0effc7b10f4ea9a338c24337e1ad97ff69d3ebacc4681b7e3e80d1b24ac languageName: node linkType: hard -"@jest/globals@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/globals@npm:30.0.5" +"@jest/globals@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/globals@npm:30.1.2" dependencies: - "@jest/environment": "npm:30.0.5" - "@jest/expect": "npm:30.0.5" + "@jest/environment": "npm:30.1.2" + "@jest/expect": "npm:30.1.2" "@jest/types": "npm:30.0.5" jest-mock: "npm:30.0.5" - checksum: 10c0/abe8e4b11f30c2885e42afa9e01d4364db8c6de4c3221f411b00a9081d3cc67226f84775efbbd17735dedb391222253f945ee260714d78b2a7304b7afa61b6d8 + checksum: 10c0/f743e83d5be14bfff171b04fa59a1d624a6f7086e6472e83c8e6a5d156e91e2ac076a826063ef4dc3045df453108aed4f17baa888d74a0aeb71517ded0791cfd languageName: node linkType: hard @@ -7229,6 +7413,42 @@ __metadata: languageName: node linkType: hard +"@jest/reporters@npm:30.1.3, @jest/reporters@npm:^30.0.2": + version: 30.1.3 + resolution: "@jest/reporters@npm:30.1.3" + dependencies: + "@bcoe/v8-coverage": "npm:^0.2.3" + "@jest/console": "npm:30.1.2" + "@jest/test-result": "npm:30.1.3" + "@jest/transform": "npm:30.1.2" + "@jest/types": "npm:30.0.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + "@types/node": "npm:*" + chalk: "npm:^4.1.2" + collect-v8-coverage: "npm:^1.0.2" + exit-x: "npm:^0.2.2" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^6.0.0" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^5.0.0" + istanbul-reports: "npm:^3.1.3" + jest-message-util: "npm:30.1.0" + jest-util: "npm:30.0.5" + jest-worker: "npm:30.1.0" + slash: "npm:^3.0.0" + string-length: "npm:^4.0.2" + v8-to-istanbul: "npm:^9.0.1" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 10c0/2b027e775216804b4f3766105bd4eb3e8e31480f78cc9a409ae7d335cc21883f5bd063c66215c0bc18cbcdda98824d1b02e74593f6464caf8920f0804ce706bb + languageName: node + linkType: hard + "@jest/reporters@npm:^29.7.0": version: 29.7.0 resolution: "@jest/reporters@npm:29.7.0" @@ -7266,42 +7486,6 @@ __metadata: languageName: node linkType: hard -"@jest/reporters@npm:^30.0.2": - version: 30.0.5 - resolution: "@jest/reporters@npm:30.0.5" - dependencies: - "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:30.0.5" - "@jest/test-result": "npm:30.0.5" - "@jest/transform": "npm:30.0.5" - "@jest/types": "npm:30.0.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" - "@types/node": "npm:*" - chalk: "npm:^4.1.2" - collect-v8-coverage: "npm:^1.0.2" - exit-x: "npm:^0.2.2" - glob: "npm:^10.3.10" - graceful-fs: "npm:^4.2.11" - istanbul-lib-coverage: "npm:^3.0.0" - istanbul-lib-instrument: "npm:^6.0.0" - istanbul-lib-report: "npm:^3.0.0" - istanbul-lib-source-maps: "npm:^5.0.0" - istanbul-reports: "npm:^3.1.3" - jest-message-util: "npm:30.0.5" - jest-util: "npm:30.0.5" - jest-worker: "npm:30.0.5" - slash: "npm:^3.0.0" - string-length: "npm:^4.0.2" - v8-to-istanbul: "npm:^9.0.1" - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - checksum: 10c0/9f8a214ff69427b644e26981fa92af49b77819d512ac17d0b4190d1dc110b0bebeb7791faa7548b8097f010b094c3b5e3244e18f3837a3fe8385ff60c7114539 - languageName: node - linkType: hard - "@jest/schemas@npm:30.0.0-beta.3": version: 30.0.0-beta.3 resolution: "@jest/schemas@npm:30.0.0-beta.3" @@ -7329,15 +7513,15 @@ __metadata: languageName: node linkType: hard -"@jest/snapshot-utils@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/snapshot-utils@npm:30.0.5" +"@jest/snapshot-utils@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/snapshot-utils@npm:30.1.2" dependencies: "@jest/types": "npm:30.0.5" chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" natural-compare: "npm:^1.4.0" - checksum: 10c0/db270c2d6e216d132c5e0b05d8ff5bbe4fbd4e65b2de4cf94eacb44152e8f17fbbba8bdd2cb83b5fc2b1094db6424c7e1507b7eaade518dbc815cfacbdf6598b + checksum: 10c0/bb5bbb3b3d0560ed1bf9439eb14280c80ead6534b7a985dbbfc656940ca140431f0719bdb7051df2a5fa897e7166f5c1902481dc57938fd6bb22b08ee7d6e09b languageName: node linkType: hard @@ -7363,15 +7547,15 @@ __metadata: languageName: node linkType: hard -"@jest/test-result@npm:30.0.5, @jest/test-result@npm:^30.0.2": - version: 30.0.5 - resolution: "@jest/test-result@npm:30.0.5" +"@jest/test-result@npm:30.1.3, @jest/test-result@npm:^30.0.2": + version: 30.1.3 + resolution: "@jest/test-result@npm:30.1.3" dependencies: - "@jest/console": "npm:30.0.5" + "@jest/console": "npm:30.1.2" "@jest/types": "npm:30.0.5" "@types/istanbul-lib-coverage": "npm:^2.0.6" collect-v8-coverage: "npm:^1.0.2" - checksum: 10c0/2a43134ee28616a178b5a6379c837f2fb054a5e4a6ab411b9d15b85224e5d459d88902cdbf83edf5821c2c77fe13e67d078eff64c6871f3b08ebff0548a9a2e4 + checksum: 10c0/610982f31d0c83d3bc9497cf4b56dacde3af6909b70dcbc986afb8e85c665061788b9d98f347412b8345cd6b2de6293c4fbde66a4bcbbf6fbb6de6a6905d8dde languageName: node linkType: hard @@ -7387,15 +7571,15 @@ __metadata: languageName: node linkType: hard -"@jest/test-sequencer@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/test-sequencer@npm:30.0.5" +"@jest/test-sequencer@npm:30.1.3": + version: 30.1.3 + resolution: "@jest/test-sequencer@npm:30.1.3" dependencies: - "@jest/test-result": "npm:30.0.5" + "@jest/test-result": "npm:30.1.3" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.0.5" + jest-haste-map: "npm:30.1.0" slash: "npm:^3.0.0" - checksum: 10c0/3caaea0558474764cd616f38acdc22ff4ce6ef806d931134ed366429fdea7110352b89d702e9cc1d71fa142d79e86f2f4e6eb0441a76a1896682e124ed8f42b4 + checksum: 10c0/ed9b24e3b37f5a6f3ae79b72fd0f241ddb422697c56b7fc49bf8b2ae3171ad466b6423a4965043ec224cdbac98c55009b585c0d79daa4ace288a2ed3ef3c38c0 languageName: node linkType: hard @@ -7411,9 +7595,9 @@ __metadata: languageName: node linkType: hard -"@jest/transform@npm:30.0.5": - version: 30.0.5 - resolution: "@jest/transform@npm:30.0.5" +"@jest/transform@npm:30.1.2": + version: 30.1.2 + resolution: "@jest/transform@npm:30.1.2" dependencies: "@babel/core": "npm:^7.27.4" "@jest/types": "npm:30.0.5" @@ -7423,14 +7607,14 @@ __metadata: convert-source-map: "npm:^2.0.0" fast-json-stable-stringify: "npm:^2.1.0" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.0.5" + jest-haste-map: "npm:30.1.0" jest-regex-util: "npm:30.0.1" jest-util: "npm:30.0.5" micromatch: "npm:^4.0.8" pirates: "npm:^4.0.7" slash: "npm:^3.0.0" write-file-atomic: "npm:^5.0.1" - checksum: 10c0/771f57b1bede66049de80dcbf984c74b7d3c072e905f2516ff3f86dc01abd2f79d821b9a6ae21f27cb26d484cd539c13b1a51f71c15e1aed0c62314203c5a186 + checksum: 10c0/b427614659a982515efcb52ac20c28c02cca133b4602549c205dda08222e5e9ed8807181d28e076e158b69fc9f8cc6a5f481e9a44c67f6fa6a64829458c5c9e3 languageName: node linkType: hard @@ -18518,6 +18702,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-darwin-arm64@npm:1.13.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-darwin-x64@npm:1.13.3" @@ -18525,6 +18716,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-darwin-x64@npm:1.13.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.3" @@ -18532,6 +18730,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-gnu@npm:1.13.3" @@ -18539,6 +18744,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm64-gnu@npm:1.13.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-arm64-musl@npm:1.13.3" @@ -18546,6 +18758,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-arm64-musl@npm:1.13.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-gnu@npm:1.13.3" @@ -18553,6 +18772,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-x64-gnu@npm:1.13.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-linux-x64-musl@npm:1.13.3" @@ -18560,6 +18786,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-linux-x64-musl@npm:1.13.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-arm64-msvc@npm:1.13.3" @@ -18567,6 +18800,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-arm64-msvc@npm:1.13.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-ia32-msvc@npm:1.13.3" @@ -18574,6 +18814,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-ia32-msvc@npm:1.13.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.13.3": version: 1.13.3 resolution: "@swc/core-win32-x64-msvc@npm:1.13.3" @@ -18581,7 +18828,14 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:1.13.3, @swc/core@npm:^1.12.11, @swc/core@npm:^1.5.22": +"@swc/core-win32-x64-msvc@npm:1.13.5": + version: 1.13.5 + resolution: "@swc/core-win32-x64-msvc@npm:1.13.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@swc/core@npm:1.13.3": version: 1.13.3 resolution: "@swc/core@npm:1.13.3" dependencies: @@ -18627,6 +18881,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.12.11, @swc/core@npm:^1.5.22, @swc/core@npm:^1.9.3": + version: 1.13.5 + resolution: "@swc/core@npm:1.13.5" + dependencies: + "@swc/core-darwin-arm64": "npm:1.13.5" + "@swc/core-darwin-x64": "npm:1.13.5" + "@swc/core-linux-arm-gnueabihf": "npm:1.13.5" + "@swc/core-linux-arm64-gnu": "npm:1.13.5" + "@swc/core-linux-arm64-musl": "npm:1.13.5" + "@swc/core-linux-x64-gnu": "npm:1.13.5" + "@swc/core-linux-x64-musl": "npm:1.13.5" + "@swc/core-win32-arm64-msvc": "npm:1.13.5" + "@swc/core-win32-ia32-msvc": "npm:1.13.5" + "@swc/core-win32-x64-msvc": "npm:1.13.5" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.24" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10c0/26efc58d2b154050a4d75b215007e2780fc02ccdd35168746e818ef58009201878d5fbe0e074ed2902858c447fd8806e52c03dd05c20ba8501573f57ef34c3be + languageName: node + linkType: hard + "@swc/counter@npm:0.1.3, @swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -18662,7 +18962,7 @@ __metadata: languageName: node linkType: hard -"@swc/jest@npm:^0.2.23, @swc/jest@npm:^0.2.39": +"@swc/jest@npm:^0.2.23, @swc/jest@npm:^0.2.36, @swc/jest@npm:^0.2.39": version: 0.2.39 resolution: "@swc/jest@npm:0.2.39" dependencies: @@ -18684,12 +18984,12 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.23": - version: 0.1.24 - resolution: "@swc/types@npm:0.1.24" +"@swc/types@npm:^0.1.23, @swc/types@npm:^0.1.24": + version: 0.1.25 + resolution: "@swc/types@npm:0.1.25" dependencies: "@swc/counter": "npm:^0.1.3" - checksum: 10c0/4ca95a338f070f48303e705996bacfc1219f606c45274bed4f6e3488b86b7b20397bd52792e58fdea0fa924fc939695b5eb5ff7f3ff4737382148fe6097e235a + checksum: 10c0/847a5b20b131281f89d640a7ed4887fb65724807d53d334b230e84b98c21097aa10cd28a074f9ed287a6ce109e443dd4bafbe7dcfb62333d7806c4ea3e7f8aca languageName: node linkType: hard @@ -19734,6 +20034,16 @@ __metadata: languageName: node linkType: hard +"@types/fs-extra@npm:^11.0.0, @types/fs-extra@npm:^11.0.4": + version: 11.0.4 + resolution: "@types/fs-extra@npm:11.0.4" + dependencies: + "@types/jsonfile": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/9e34f9b24ea464f3c0b18c3f8a82aefc36dc524cc720fc2b886e5465abc66486ff4e439ea3fb2c0acebf91f6d3f74e514f9983b1f02d4243706bdbb7511796ad + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -19854,6 +20164,16 @@ __metadata: languageName: node linkType: hard +"@types/inquirer@npm:^9.0.0, @types/inquirer@npm:^9.0.9": + version: 9.0.9 + resolution: "@types/inquirer@npm:9.0.9" + dependencies: + "@types/through": "npm:*" + rxjs: "npm:^7.2.0" + checksum: 10c0/235a02a3afa5b238ca9093ef7064e17d763ba134b1afd5a263ffc363ccdb6d1f7d64aa3866ca93e7ad52be4ff21324368a983d20d35caf21c16475cfb32db8c8 + languageName: node + linkType: hard + "@types/is-hotkey@npm:^0.1.1": version: 0.1.10 resolution: "@types/is-hotkey@npm:0.1.10" @@ -19886,6 +20206,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.0": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: "npm:^29.0.0" + pretty-format: "npm:^29.0.0" + checksum: 10c0/18e0712d818890db8a8dab3d91e9ea9f7f19e3f83c2e50b312f557017dc81466207a71f3ed79cf4428e813ba939954fa26ffa0a9a7f153181ba174581b1c2aed + languageName: node + linkType: hard + "@types/jest@npm:^30.0.0": version: 30.0.0 resolution: "@types/jest@npm:30.0.0" @@ -19942,6 +20272,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonfile@npm:*": + version: 6.1.4 + resolution: "@types/jsonfile@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/b12d068b021e4078f6ac4441353965769be87acf15326173e2aea9f3bf8ead41bd0ad29421df5bbeb0123ec3fc02eb0a734481d52903704a1454a1845896b9eb + languageName: node + linkType: hard + "@types/jsonwebtoken@npm:*": version: 9.0.6 resolution: "@types/jsonwebtoken@npm:9.0.6" @@ -20366,12 +20705,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=8.1.0, @types/node@npm:^24.0.0": - version: 24.2.0 - resolution: "@types/node@npm:24.2.0" +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=13.7.0, @types/node@npm:>=8.1.0, @types/node@npm:^24.0.0, @types/node@npm:^24.3.1": + version: 24.3.1 + resolution: "@types/node@npm:24.3.1" dependencies: undici-types: "npm:~7.10.0" - checksum: 10c0/0b55af4d7b37fea47bbeffffaff908462fa19ea9b1a18f92d9ed6d8415d97971b254f8cb3f629cd238916e94711fdb6ac939aa750cb353dfd6df6c0339435740 + checksum: 10c0/99b86fc32294fcd61136ca1f771026443a1e370e9f284f75e243b29299dd878e18c193deba1ce29a374932db4e30eb80826e1049b9aad02d36f5c30b94b6f928 languageName: node linkType: hard @@ -20398,21 +20737,21 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.3.1": - version: 20.14.14 - resolution: "@types/node@npm:20.14.14" +"@types/node@npm:^20.0.0, @types/node@npm:^20.3.1": + version: 20.19.13 + resolution: "@types/node@npm:20.19.13" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10c0/4fc8d368df2b6f5497698327b30db68d7d20e32221ce7d057fb15cbd5834685b2fde0440609e4cb2204e5d305b928f008faf41b950a425f3fd55b60cb1b997cf + undici-types: "npm:~6.21.0" + checksum: 10c0/25fba13d50c4f18ac56a50d4491502e7d095f09e44c17c0c6887aa5e4e6122e961e92fc3b682ed8c053ab87a05ce10f501345d23d96fdfba8002ecce6c8ced51 languageName: node linkType: hard -"@types/node@npm:^22.1.0, @types/node@npm:^22.7.5": - version: 22.13.17 - resolution: "@types/node@npm:22.13.17" +"@types/node@npm:^22.5.5, @types/node@npm:^22.7.5": + version: 22.18.1 + resolution: "@types/node@npm:22.18.1" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10c0/77a052fec0fe02f60557e1c5f3f28eb09cd9bee426be88328a94689150a3c0df5b4b6b69fad28157fb34521693dad0b311ecd7f613845d681ff973991310c20e + undici-types: "npm:~6.21.0" + checksum: 10c0/1912b0ea6cb9ef59722b0fed64652388e13b41d52569c16198f1278a882837bbf4c8a4ec913e852893356f07c0c44b4e00fbca289ac7222741d03449104e22fe languageName: node linkType: hard @@ -20770,6 +21109,15 @@ __metadata: languageName: node linkType: hard +"@types/through@npm:*": + version: 0.0.33 + resolution: "@types/through@npm:0.0.33" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/6a8edd7f40cd7e197318e86310a40e568cddd380609dde59b30d5cc6c5f8276ddc698905eac4b3b429eb39f2e8ee326bc20dc6e95a2cdc41c4d3fc9a1ebd4929 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*, @types/tough-cookie@npm:^4.0.5": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -22652,7 +23000,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0, ajv@npm:^8.12.0, ajv@npm:^8.17.1, ajv@npm:^8.9.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -23598,7 +23946,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3": +"axios@npm:^1.6.0, axios@npm:^1.6.1, axios@npm:^1.7.7, axios@npm:^1.8.2, axios@npm:^1.8.3": version: 1.11.0 resolution: "axios@npm:1.11.0" dependencies: @@ -23846,11 +24194,11 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:30.0.5": - version: 30.0.5 - resolution: "babel-jest@npm:30.0.5" +"babel-jest@npm:30.1.2": + version: 30.1.2 + resolution: "babel-jest@npm:30.1.2" dependencies: - "@jest/transform": "npm:30.0.5" + "@jest/transform": "npm:30.1.2" "@types/babel__core": "npm:^7.20.5" babel-plugin-istanbul: "npm:^7.0.0" babel-preset-jest: "npm:30.0.1" @@ -23859,7 +24207,7 @@ __metadata: slash: "npm:^3.0.0" peerDependencies: "@babel/core": ^7.11.0 - checksum: 10c0/48fcdbf97519216f8897c4d83c0d2a64dffd90e4876b386e4ea4530021aaedbd7253de65a71d554cb57fdeb7bd8509bed43a6c016eb150e49e1fbe1236248f0f + checksum: 10c0/c0f25d637708beeb210fc27b87a50bd4693de2e88b0ae89ea255fc1906dea362fde6ae81f602e3cd3c6b439eb7553bf0f3fca7b8f79681f6e2f1df8ec9576b00 languageName: node linkType: hard @@ -26079,6 +26427,13 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^2.1.0": + version: 2.1.0 + resolution: "chardet@npm:2.1.0" + checksum: 10c0/d1b03e47371851ed72741a898281d58f8a9b577aeea6fdfa75a86832898b36c550b3ad057e66d50d774a9cebd9f56c66b6880e4fe75e387794538ba7565b0b6f + languageName: node + linkType: hard + "check-disk-space@npm:3.4.0": version: 3.4.0 resolution: "check-disk-space@npm:3.4.0" @@ -26159,7 +26514,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:4.0.3, chokidar@npm:^4.0.3": +"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" dependencies: @@ -26416,6 +26771,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^5.0.0": + version: 5.0.0 + resolution: "cli-cursor@npm:5.0.0" + dependencies: + restore-cursor: "npm:^5.0.0" + checksum: 10c0/7ec62f69b79f6734ab209a3e4dbdc8af7422d44d360a7cb1efa8a0887bbe466a6e625650c466fe4359aee44dbe2dc0b6994b583d40a05d0808a5cb193641d220 + languageName: node + linkType: hard + "cli-highlight@npm:^2.1.11": version: 2.1.11 resolution: "cli-highlight@npm:2.1.11" @@ -26943,6 +27307,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.0.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^2.18.0, commander@npm:^2.20.0, commander@npm:^2.20.3": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -29182,7 +29553,14 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0, dotenv@npm:^16.4.5, dotenv@npm:~16.4.5": +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0, dotenv@npm:^16.4.0, dotenv@npm:^16.4.5": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc + languageName: node + linkType: hard + +"dotenv@npm:~16.4.5": version: 16.4.7 resolution: "dotenv@npm:16.4.7" checksum: 10c0/be9f597e36a8daf834452daa1f4cc30e5375a5968f98f46d89b16b983c567398a330580c88395069a77473943c06b877d1ca25b4afafcdd6d4adb549e8293462 @@ -31252,21 +31630,21 @@ __metadata: languageName: node linkType: hard -"expect@npm:30.0.5, expect@npm:^30.0.0": - version: 30.0.5 - resolution: "expect@npm:30.0.5" +"expect@npm:30.1.2, expect@npm:^30.0.0": + version: 30.1.2 + resolution: "expect@npm:30.1.2" dependencies: - "@jest/expect-utils": "npm:30.0.5" - "@jest/get-type": "npm:30.0.1" - jest-matcher-utils: "npm:30.0.5" - jest-message-util: "npm:30.0.5" + "@jest/expect-utils": "npm:30.1.2" + "@jest/get-type": "npm:30.1.0" + jest-matcher-utils: "npm:30.1.2" + jest-message-util: "npm:30.1.0" jest-mock: "npm:30.0.5" jest-util: "npm:30.0.5" - checksum: 10c0/e08e4ced2856a0898b3a4e8d09aab7f8e2212cde701e41a560c3ab7e9053517947ff1a762fc425dbe0c48ed54e131aa7190de67a402f98b4e5ada23eb21c0a9f + checksum: 10c0/467c1b69549e75a1a09f3feec335e0dc968cd71370361b5d83248351cf77e705e8ddf38a4885e32a50237502ced7fcc9106462f59f33c4796462e95938b8ca19 languageName: node linkType: hard -"expect@npm:^29.7.0": +"expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" dependencies: @@ -32732,6 +33110,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.4.0 + resolution: "get-east-asian-width@npm:1.4.0" + checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83 + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -34889,7 +35274,7 @@ __metadata: languageName: node linkType: hard -"import-local@npm:^3.0.2": +"import-local@npm:^3.0.2, import-local@npm:^3.2.0": version: 3.2.0 resolution: "import-local@npm:3.2.0" dependencies: @@ -35032,7 +35417,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:8.2.6, inquirer@npm:^8.0.0": +"inquirer@npm:8.2.6": version: 8.2.6 resolution: "inquirer@npm:8.2.6" dependencies: @@ -35055,6 +35440,29 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:8.2.7, inquirer@npm:^8.0.0": + version: 8.2.7 + resolution: "inquirer@npm:8.2.7" + dependencies: + "@inquirer/external-editor": "npm:^1.0.0" + ansi-escapes: "npm:^4.2.1" + chalk: "npm:^4.1.1" + cli-cursor: "npm:^3.1.0" + cli-width: "npm:^3.0.0" + figures: "npm:^3.0.0" + lodash: "npm:^4.17.21" + mute-stream: "npm:0.0.8" + ora: "npm:^5.4.1" + run-async: "npm:^2.4.0" + rxjs: "npm:^7.5.5" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + through: "npm:^2.3.6" + wrap-ansi: "npm:^6.0.1" + checksum: 10c0/75aa594231769d292102615da3199320359bfb566e96dae0f89a5773a18e21c676709d9f5a9fb1372f7d2cf25c551a4efe53691ff436d941f95336931777c15d + languageName: node + linkType: hard + "inquirer@npm:9.2.11": version: 9.2.11 resolution: "inquirer@npm:9.2.11" @@ -35078,6 +35486,22 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^10.0.0": + version: 10.2.2 + resolution: "inquirer@npm:10.2.2" + dependencies: + "@inquirer/core": "npm:^9.1.0" + "@inquirer/prompts": "npm:^5.5.0" + "@inquirer/type": "npm:^1.5.3" + "@types/mute-stream": "npm:^0.0.4" + ansi-escapes: "npm:^4.3.2" + mute-stream: "npm:^1.0.0" + run-async: "npm:^3.0.0" + rxjs: "npm:^7.8.1" + checksum: 10c0/09bcff887a968ce29d3a8cba749ef35b6531b7fb7bf5b28aaf3e10aebae07af2494dd3ec67ae6f1dabe76da1064cfe4514098d4c7658fcaf3fd480b2975d7163 + languageName: node + linkType: hard + "inquirer@npm:^7.3.3": version: 7.3.3 resolution: "inquirer@npm:7.3.3" @@ -35612,6 +36036,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10c0/801c8f6064f85199dc6bf99b5dd98db3282e930c3bc197b32f2c5b89313bb578a07d1b8a01365c4348c2927229234f3681eb861b9c2c92bee72ff397390fa600 + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -35934,13 +36365,20 @@ __metadata: languageName: node linkType: hard -"is-unicode-supported@npm:^1.2.0": +"is-unicode-supported@npm:^1.2.0, is-unicode-supported@npm:^1.3.0": version: 1.3.0 resolution: "is-unicode-supported@npm:1.3.0" checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a languageName: node linkType: hard +"is-unicode-supported@npm:^2.0.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5 + languageName: node + linkType: hard + "is-upper-case@npm:^2.0.2": version: 2.0.2 resolution: "is-upper-case@npm:2.0.2" @@ -36305,6 +36743,17 @@ __metadata: languageName: node linkType: hard +"jest-changed-files@npm:30.0.5": + version: 30.0.5 + resolution: "jest-changed-files@npm:30.0.5" + dependencies: + execa: "npm:^5.1.1" + jest-util: "npm:30.0.5" + p-limit: "npm:^3.1.0" + checksum: 10c0/41ce090f324e8450443327f19f772a9c3f225b4b1374ba9704358f0c8b8cd91fd134fa41df7db4d278428ab974c432abc3eca9484e67c8f18528974378fddef6 + languageName: node + linkType: hard + "jest-changed-files@npm:^29.7.0": version: 29.7.0 resolution: "jest-changed-files@npm:29.7.0" @@ -36316,31 +36765,31 @@ __metadata: languageName: node linkType: hard -"jest-circus@npm:30.0.5": - version: 30.0.5 - resolution: "jest-circus@npm:30.0.5" +"jest-circus@npm:30.1.3": + version: 30.1.3 + resolution: "jest-circus@npm:30.1.3" dependencies: - "@jest/environment": "npm:30.0.5" - "@jest/expect": "npm:30.0.5" - "@jest/test-result": "npm:30.0.5" + "@jest/environment": "npm:30.1.2" + "@jest/expect": "npm:30.1.2" + "@jest/test-result": "npm:30.1.3" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" chalk: "npm:^4.1.2" co: "npm:^4.6.0" dedent: "npm:^1.6.0" is-generator-fn: "npm:^2.1.0" - jest-each: "npm:30.0.5" - jest-matcher-utils: "npm:30.0.5" - jest-message-util: "npm:30.0.5" - jest-runtime: "npm:30.0.5" - jest-snapshot: "npm:30.0.5" + jest-each: "npm:30.1.0" + jest-matcher-utils: "npm:30.1.2" + jest-message-util: "npm:30.1.0" + jest-runtime: "npm:30.1.3" + jest-snapshot: "npm:30.1.2" jest-util: "npm:30.0.5" p-limit: "npm:^3.1.0" pretty-format: "npm:30.0.5" pure-rand: "npm:^7.0.0" slash: "npm:^3.0.0" stack-utils: "npm:^2.0.6" - checksum: 10c0/028204897eee7bef2d04eea0216b48f94e3da77ff1d12b0e3a5e265e8e73bcd31192cec70282aa1ece91150c00fcb5662c2c68e86b3892cffbfbe7058fa7f4e5 + checksum: 10c0/9bea7baf7daf814f3b494363e4ce321d98a2229078b6aff07f3a5623d93c99be11c9d07c0cbb75dcc9b4293dc13535c4c400500e69f08e869ed964b098fab3c8 languageName: node linkType: hard @@ -36372,6 +36821,31 @@ __metadata: languageName: node linkType: hard +"jest-cli@npm:30.1.3": + version: 30.1.3 + resolution: "jest-cli@npm:30.1.3" + dependencies: + "@jest/core": "npm:30.1.3" + "@jest/test-result": "npm:30.1.3" + "@jest/types": "npm:30.0.5" + chalk: "npm:^4.1.2" + exit-x: "npm:^0.2.2" + import-local: "npm:^3.2.0" + jest-config: "npm:30.1.3" + jest-util: "npm:30.0.5" + jest-validate: "npm:30.1.0" + yargs: "npm:^17.7.2" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: ./bin/jest.js + checksum: 10c0/e15e811b0a14625ce70b1b3948955e9a9c7a523b2c7970effa8f924006dc4d1839b5636fa5c7c5c772d7959ed331a45b0e9d85d3b71f9a065aa6440ae3c1aa64 + languageName: node + linkType: hard + "jest-cli@npm:^29.7.0": version: 29.7.0 resolution: "jest-cli@npm:29.7.0" @@ -36398,6 +36872,49 @@ __metadata: languageName: node linkType: hard +"jest-config@npm:30.1.3, jest-config@npm:^30.0.2": + version: 30.1.3 + resolution: "jest-config@npm:30.1.3" + dependencies: + "@babel/core": "npm:^7.27.4" + "@jest/get-type": "npm:30.1.0" + "@jest/pattern": "npm:30.0.1" + "@jest/test-sequencer": "npm:30.1.3" + "@jest/types": "npm:30.0.5" + babel-jest: "npm:30.1.2" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + deepmerge: "npm:^4.3.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + jest-circus: "npm:30.1.3" + jest-docblock: "npm:30.0.1" + jest-environment-node: "npm:30.1.2" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.1.3" + jest-runner: "npm:30.1.3" + jest-util: "npm:30.0.5" + jest-validate: "npm:30.1.0" + micromatch: "npm:^4.0.8" + parse-json: "npm:^5.2.0" + pretty-format: "npm:30.0.5" + slash: "npm:^3.0.0" + strip-json-comments: "npm:^3.1.1" + peerDependencies: + "@types/node": "*" + esbuild-register: ">=3.4.0" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + checksum: 10c0/9e347a22232d0368acad44719b1f6f3d5a7a39fd26156387c898904a3477e52267a5078624e7a3d231f05005679e21daaf080dec936ad44ac1f242500cd8d2bc + languageName: node + linkType: hard + "jest-config@npm:^29.7.0": version: 29.7.0 resolution: "jest-config@npm:29.7.0" @@ -36436,58 +36953,15 @@ __metadata: languageName: node linkType: hard -"jest-config@npm:^30.0.2": - version: 30.0.5 - resolution: "jest-config@npm:30.0.5" - dependencies: - "@babel/core": "npm:^7.27.4" - "@jest/get-type": "npm:30.0.1" - "@jest/pattern": "npm:30.0.1" - "@jest/test-sequencer": "npm:30.0.5" - "@jest/types": "npm:30.0.5" - babel-jest: "npm:30.0.5" - chalk: "npm:^4.1.2" - ci-info: "npm:^4.2.0" - deepmerge: "npm:^4.3.1" - glob: "npm:^10.3.10" - graceful-fs: "npm:^4.2.11" - jest-circus: "npm:30.0.5" - jest-docblock: "npm:30.0.1" - jest-environment-node: "npm:30.0.5" - jest-regex-util: "npm:30.0.1" - jest-resolve: "npm:30.0.5" - jest-runner: "npm:30.0.5" - jest-util: "npm:30.0.5" - jest-validate: "npm:30.0.5" - micromatch: "npm:^4.0.8" - parse-json: "npm:^5.2.0" - pretty-format: "npm:30.0.5" - slash: "npm:^3.0.0" - strip-json-comments: "npm:^3.1.1" - peerDependencies: - "@types/node": "*" - esbuild-register: ">=3.4.0" - ts-node: ">=9.0.0" - peerDependenciesMeta: - "@types/node": - optional: true - esbuild-register: - optional: true - ts-node: - optional: true - checksum: 10c0/da68048801e6f6622bf6e9a361dcfb3859017bbd58fabcf53bade41157bdf31cc35a1bd3dab1e3cca86e69da23e2c27c7aa5e308efc04564a454e23de6f22062 - languageName: node - linkType: hard - -"jest-diff@npm:30.0.5, jest-diff@npm:^30.0.2": - version: 30.0.5 - resolution: "jest-diff@npm:30.0.5" +"jest-diff@npm:30.1.2, jest-diff@npm:^30.0.2": + version: 30.1.2 + resolution: "jest-diff@npm:30.1.2" dependencies: "@jest/diff-sequences": "npm:30.0.1" - "@jest/get-type": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" chalk: "npm:^4.1.2" pretty-format: "npm:30.0.5" - checksum: 10c0/b218ced37b7676f578ea866762f04caa74901bdcf3f593872aa9a4991a586302651a1d16bb0386772adacc7580a452ec621359af75d733c0b50ea947fe1881d3 + checksum: 10c0/5baba5c54d044faf77540d2b97f947ce2a735c529bdca23ccd25669085ba3912eef2a8f66f4d765e8e416b1e10b95cb1dded0ebc1633efdbef37706b4e767ecb languageName: node linkType: hard @@ -36521,16 +36995,16 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:30.0.5": - version: 30.0.5 - resolution: "jest-each@npm:30.0.5" +"jest-each@npm:30.1.0": + version: 30.1.0 + resolution: "jest-each@npm:30.1.0" dependencies: - "@jest/get-type": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" "@jest/types": "npm:30.0.5" chalk: "npm:^4.1.2" jest-util: "npm:30.0.5" pretty-format: "npm:30.0.5" - checksum: 10c0/fe7509bfd8b0c8553bbdaffda5d3b674a4da870c5ce9fe69c1ca8111d9e0f21a8f265799eba0f927581d16f4810e5eb5bebfd7e51f5f137cbef08cc44d8fd9cd + checksum: 10c0/2db0fe6df0084b6e9e1eda8f024990c66ade49845f4259f2fd0bc64f31c709a49d66b8f3d7b4b09064bbd9c2acf8fdd320b9b29801050875d422b3f1004b6f7b languageName: node linkType: hard @@ -36565,18 +37039,18 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:30.0.5": - version: 30.0.5 - resolution: "jest-environment-node@npm:30.0.5" +"jest-environment-node@npm:30.1.2": + version: 30.1.2 + resolution: "jest-environment-node@npm:30.1.2" dependencies: - "@jest/environment": "npm:30.0.5" - "@jest/fake-timers": "npm:30.0.5" + "@jest/environment": "npm:30.1.2" + "@jest/fake-timers": "npm:30.1.2" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" jest-mock: "npm:30.0.5" jest-util: "npm:30.0.5" - jest-validate: "npm:30.0.5" - checksum: 10c0/1b608597f0755814e7c24b9ed2a45abc2340cfd8f8d3691caf929f332facd9c62ac5092e7f01056708a0ca41ae0458b6d442fd1ae9f6d21b7b416b252e1ae210 + jest-validate: "npm:30.1.0" + checksum: 10c0/6298269ba9e132634cc1548391a744387e8035f9b107cdd5250e6a813080c4099386ce55de8bdc68a12232f08d185d459b9517c50154107c8e070d49fcd8e879 languageName: node linkType: hard @@ -36611,9 +37085,9 @@ __metadata: languageName: node linkType: hard -"jest-haste-map@npm:30.0.5": - version: 30.0.5 - resolution: "jest-haste-map@npm:30.0.5" +"jest-haste-map@npm:30.1.0": + version: 30.1.0 + resolution: "jest-haste-map@npm:30.1.0" dependencies: "@jest/types": "npm:30.0.5" "@types/node": "npm:*" @@ -36623,13 +37097,13 @@ __metadata: graceful-fs: "npm:^4.2.11" jest-regex-util: "npm:30.0.1" jest-util: "npm:30.0.5" - jest-worker: "npm:30.0.5" + jest-worker: "npm:30.1.0" micromatch: "npm:^4.0.8" walker: "npm:^1.0.8" dependenciesMeta: fsevents: optional: true - checksum: 10c0/eab5d85d820f149bcf4bf4e0c49316f48973c85d39b4c3a2e08f57504f069afe9b0f1665e556330a98c6fc6bd5a6932767b466c1c96124fa0161aef017ab17b3 + checksum: 10c0/a6001e350b0f1b4de6e4855130c516e3848c49fe90c50562f0eca525b80494f0d8ac1cc4d99013bdda9d2a5bd015e70abbcf3dbbf43b711fd39eaf7d47370220 languageName: node linkType: hard @@ -36668,13 +37142,13 @@ __metadata: languageName: node linkType: hard -"jest-leak-detector@npm:30.0.5": - version: 30.0.5 - resolution: "jest-leak-detector@npm:30.0.5" +"jest-leak-detector@npm:30.1.0": + version: 30.1.0 + resolution: "jest-leak-detector@npm:30.1.0" dependencies: - "@jest/get-type": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" pretty-format: "npm:30.0.5" - checksum: 10c0/04207ab6f44dec22d3d656b5f3b4f334440f4c01ccd21c55474f26706530244d34b8dc9922c9449e00e8649e5da1b8de4aca58c9895c9de19951d5ecdc0ff113 + checksum: 10c0/a0b0c30988abab2efdf99fe8e00120c0b57406072efe2b1380270d0df4866812efe85903155682e2ec773164d8d0213e5ff59df1d8b343245dd4522d332c2853 languageName: node linkType: hard @@ -36688,15 +37162,15 @@ __metadata: languageName: node linkType: hard -"jest-matcher-utils@npm:30.0.5": - version: 30.0.5 - resolution: "jest-matcher-utils@npm:30.0.5" +"jest-matcher-utils@npm:30.1.2": + version: 30.1.2 + resolution: "jest-matcher-utils@npm:30.1.2" dependencies: - "@jest/get-type": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" chalk: "npm:^4.1.2" - jest-diff: "npm:30.0.5" + jest-diff: "npm:30.1.2" pretty-format: "npm:30.0.5" - checksum: 10c0/231d891b29bfc218f2f5739c10873b6671426e31ad1c5538eed1531e62608fd3f60d32f41821332a6cf41f1614fd37361434c754fdd49c849b35ef2e5156c02e + checksum: 10c0/c4f81fc7d72f94b18dff807adf787d6fd081c3e150148fbbcb1559c353b27890989bcf7e10b15d763625565175bf30019e93a014078ff291646a88a9acdfc9a4 languageName: node linkType: hard @@ -36729,9 +37203,9 @@ __metadata: languageName: node linkType: hard -"jest-message-util@npm:30.0.5": - version: 30.0.5 - resolution: "jest-message-util@npm:30.0.5" +"jest-message-util@npm:30.1.0": + version: 30.1.0 + resolution: "jest-message-util@npm:30.1.0" dependencies: "@babel/code-frame": "npm:^7.27.1" "@jest/types": "npm:30.0.5" @@ -36742,7 +37216,7 @@ __metadata: pretty-format: "npm:30.0.5" slash: "npm:^3.0.0" stack-utils: "npm:^2.0.6" - checksum: 10c0/38b710c127db6c79c36d690377d9f9f1e3c2e4b2d2e60f3b82a5b4da70efb1f4783c6cf0cf1f6be6e3b7fb2d2aed889583d2430f65afc09e7e6d68aa5fa981dc + checksum: 10c0/3884f7e772d64891eca63870f73b89af4e1dce715611c308e1115f7961ed378560bac66c5f9cbee025b06ca530dbd30685362cb8db7b5a48f5f53b75ba79023e languageName: node linkType: hard @@ -36866,6 +37340,16 @@ __metadata: languageName: node linkType: hard +"jest-resolve-dependencies@npm:30.1.3": + version: 30.1.3 + resolution: "jest-resolve-dependencies@npm:30.1.3" + dependencies: + jest-regex-util: "npm:30.0.1" + jest-snapshot: "npm:30.1.2" + checksum: 10c0/1fc144c3107ad7faae455ac13b32de0cabb8e2718a55f431246a222da86c6da539f80230814b1cbcaf22c06df704c3c00ab761d065b9a9241659757e3fd28f66 + languageName: node + linkType: hard + "jest-resolve-dependencies@npm:^29.7.0": version: 29.7.0 resolution: "jest-resolve-dependencies@npm:29.7.0" @@ -36876,19 +37360,19 @@ __metadata: languageName: node linkType: hard -"jest-resolve@npm:30.0.5, jest-resolve@npm:^30.0.2": - version: 30.0.5 - resolution: "jest-resolve@npm:30.0.5" +"jest-resolve@npm:30.1.3, jest-resolve@npm:^30.0.2": + version: 30.1.3 + resolution: "jest-resolve@npm:30.1.3" dependencies: chalk: "npm:^4.1.2" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.0.5" + jest-haste-map: "npm:30.1.0" jest-pnp-resolver: "npm:^1.2.3" jest-util: "npm:30.0.5" - jest-validate: "npm:30.0.5" + jest-validate: "npm:30.1.0" slash: "npm:^3.0.0" unrs-resolver: "npm:^1.7.11" - checksum: 10c0/6edea75db950131513cd642743d4c5dd36c209c94652e469eebc86fdf85eb579a7614c30262668fcd429e1c841f1d17a26831259db69c17dffd0718c37f69196 + checksum: 10c0/ba84ac234ac2fd6ee4703aa2bb6471e5b4c17af7e36c1f75aae85a5d907694703b5501d5109d0fdc8875556a5a34dbf4fd7fe8732d4ee83cbbe1d88ef9c795b1 languageName: node linkType: hard @@ -36909,14 +37393,14 @@ __metadata: languageName: node linkType: hard -"jest-runner@npm:30.0.5": - version: 30.0.5 - resolution: "jest-runner@npm:30.0.5" +"jest-runner@npm:30.1.3": + version: 30.1.3 + resolution: "jest-runner@npm:30.1.3" dependencies: - "@jest/console": "npm:30.0.5" - "@jest/environment": "npm:30.0.5" - "@jest/test-result": "npm:30.0.5" - "@jest/transform": "npm:30.0.5" + "@jest/console": "npm:30.1.2" + "@jest/environment": "npm:30.1.2" + "@jest/test-result": "npm:30.1.3" + "@jest/transform": "npm:30.1.2" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" chalk: "npm:^4.1.2" @@ -36924,18 +37408,18 @@ __metadata: exit-x: "npm:^0.2.2" graceful-fs: "npm:^4.2.11" jest-docblock: "npm:30.0.1" - jest-environment-node: "npm:30.0.5" - jest-haste-map: "npm:30.0.5" - jest-leak-detector: "npm:30.0.5" - jest-message-util: "npm:30.0.5" - jest-resolve: "npm:30.0.5" - jest-runtime: "npm:30.0.5" + jest-environment-node: "npm:30.1.2" + jest-haste-map: "npm:30.1.0" + jest-leak-detector: "npm:30.1.0" + jest-message-util: "npm:30.1.0" + jest-resolve: "npm:30.1.3" + jest-runtime: "npm:30.1.3" jest-util: "npm:30.0.5" - jest-watcher: "npm:30.0.5" - jest-worker: "npm:30.0.5" + jest-watcher: "npm:30.1.3" + jest-worker: "npm:30.1.0" p-limit: "npm:^3.1.0" source-map-support: "npm:0.5.13" - checksum: 10c0/5da84e4f393cc4b0c2b86a7058c154e524bc91947867f892d252300d06c595058690a61ffdbfa74381498f4ebb9cc7d8d967a62f53cb5f5383ec59fb5ed21d91 + checksum: 10c0/867f878892c88e8a050d2d687e44aedd661473c747c2a39e7b97297927b76c679710b229419ec3c237dfbbccf9061a3b953fa0d7ebfe4dd385c6048738658d33 languageName: node linkType: hard @@ -36968,16 +37452,16 @@ __metadata: languageName: node linkType: hard -"jest-runtime@npm:30.0.5": - version: 30.0.5 - resolution: "jest-runtime@npm:30.0.5" +"jest-runtime@npm:30.1.3": + version: 30.1.3 + resolution: "jest-runtime@npm:30.1.3" dependencies: - "@jest/environment": "npm:30.0.5" - "@jest/fake-timers": "npm:30.0.5" - "@jest/globals": "npm:30.0.5" + "@jest/environment": "npm:30.1.2" + "@jest/fake-timers": "npm:30.1.2" + "@jest/globals": "npm:30.1.2" "@jest/source-map": "npm:30.0.1" - "@jest/test-result": "npm:30.0.5" - "@jest/transform": "npm:30.0.5" + "@jest/test-result": "npm:30.1.3" + "@jest/transform": "npm:30.1.2" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" chalk: "npm:^4.1.2" @@ -36985,16 +37469,16 @@ __metadata: collect-v8-coverage: "npm:^1.0.2" glob: "npm:^10.3.10" graceful-fs: "npm:^4.2.11" - jest-haste-map: "npm:30.0.5" - jest-message-util: "npm:30.0.5" + jest-haste-map: "npm:30.1.0" + jest-message-util: "npm:30.1.0" jest-mock: "npm:30.0.5" jest-regex-util: "npm:30.0.1" - jest-resolve: "npm:30.0.5" - jest-snapshot: "npm:30.0.5" + jest-resolve: "npm:30.1.3" + jest-snapshot: "npm:30.1.2" jest-util: "npm:30.0.5" slash: "npm:^3.0.0" strip-bom: "npm:^4.0.0" - checksum: 10c0/c1afa36da0582172e9a73d69fcc23fd433efc8a7d0328ba5fee45858dc85cb01410b47ba53540bb3758277eb84bb5a42e872bc58d2e5a3cad533f4b33e3abe61 + checksum: 10c0/2b5fe84685fddaca4e25cf4d578ab2bfdef2912e970be51a083ef0a7b6a8ff66f876aa72fb709674447fa57925551e2985af52d02a8c5d73d47a57b2057b5f95 languageName: node linkType: hard @@ -37037,32 +37521,32 @@ __metadata: languageName: node linkType: hard -"jest-snapshot@npm:30.0.5": - version: 30.0.5 - resolution: "jest-snapshot@npm:30.0.5" +"jest-snapshot@npm:30.1.2": + version: 30.1.2 + resolution: "jest-snapshot@npm:30.1.2" dependencies: "@babel/core": "npm:^7.27.4" "@babel/generator": "npm:^7.27.5" "@babel/plugin-syntax-jsx": "npm:^7.27.1" "@babel/plugin-syntax-typescript": "npm:^7.27.1" "@babel/types": "npm:^7.27.3" - "@jest/expect-utils": "npm:30.0.5" - "@jest/get-type": "npm:30.0.1" - "@jest/snapshot-utils": "npm:30.0.5" - "@jest/transform": "npm:30.0.5" + "@jest/expect-utils": "npm:30.1.2" + "@jest/get-type": "npm:30.1.0" + "@jest/snapshot-utils": "npm:30.1.2" + "@jest/transform": "npm:30.1.2" "@jest/types": "npm:30.0.5" babel-preset-current-node-syntax: "npm:^1.1.0" chalk: "npm:^4.1.2" - expect: "npm:30.0.5" + expect: "npm:30.1.2" graceful-fs: "npm:^4.2.11" - jest-diff: "npm:30.0.5" - jest-matcher-utils: "npm:30.0.5" - jest-message-util: "npm:30.0.5" + jest-diff: "npm:30.1.2" + jest-matcher-utils: "npm:30.1.2" + jest-message-util: "npm:30.1.0" jest-util: "npm:30.0.5" pretty-format: "npm:30.0.5" semver: "npm:^7.7.2" synckit: "npm:^0.11.8" - checksum: 10c0/2bda246367373003abfbd66de261bfd355618926c28261d7ffcdfac0c4c7a7f575c9f598745b0b59eb2cfa8907889dcc07db3ad65d940061275d490c1eb3e1fe + checksum: 10c0/deca264b6564a5250f1f4153edefd8d626f880062e592656071507a71763b9ef9bcb88b5c011d7b18eb783d8be428ed35ab42f5f9227543dbf161cee586eeb4f languageName: node linkType: hard @@ -37136,17 +37620,17 @@ __metadata: languageName: node linkType: hard -"jest-validate@npm:30.0.5": - version: 30.0.5 - resolution: "jest-validate@npm:30.0.5" +"jest-validate@npm:30.1.0": + version: 30.1.0 + resolution: "jest-validate@npm:30.1.0" dependencies: - "@jest/get-type": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" "@jest/types": "npm:30.0.5" camelcase: "npm:^6.3.0" chalk: "npm:^4.1.2" leven: "npm:^3.1.0" pretty-format: "npm:30.0.5" - checksum: 10c0/739a5df57befd763ba40693c9c1d7e93234af44ca21226a42272fbf87dea076a23848072b46871ce02cc0f2614f8ad41542e98965b405320276102b4de35b063 + checksum: 10c0/6b8dd92e918496763827a9d440200657194b0158f70b42c9d4df373ff0504d063f342acdd12f692a8cb8610e7077c61289f934ae0c7878c9baff2d8be5efbc1f languageName: node linkType: hard @@ -37181,11 +37665,11 @@ __metadata: languageName: node linkType: hard -"jest-watcher@npm:30.0.5": - version: 30.0.5 - resolution: "jest-watcher@npm:30.0.5" +"jest-watcher@npm:30.1.3": + version: 30.1.3 + resolution: "jest-watcher@npm:30.1.3" dependencies: - "@jest/test-result": "npm:30.0.5" + "@jest/test-result": "npm:30.1.3" "@jest/types": "npm:30.0.5" "@types/node": "npm:*" ansi-escapes: "npm:^4.3.2" @@ -37193,7 +37677,7 @@ __metadata: emittery: "npm:^0.13.1" jest-util: "npm:30.0.5" string-length: "npm:^4.0.2" - checksum: 10c0/5c26617c53e6314e2143806cbc8c1cdca7100cc8de3241c7debf7b5feb0df17bdc9a92ee4a4efa953a261d8806ffd7f6c89e72d567236e62492dd554eaa91f97 + checksum: 10c0/6783c17813afeddc333967f1dcc7ae8ac46269f6c45ff33b2d3665f5b697fb1ca70968903e6a4371e70cb7eab7a7e6cc2e446941c612e275e21f2dde7a666711 languageName: node linkType: hard @@ -37213,16 +37697,16 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:30.0.5": - version: 30.0.5 - resolution: "jest-worker@npm:30.0.5" +"jest-worker@npm:30.1.0": + version: 30.1.0 + resolution: "jest-worker@npm:30.1.0" dependencies: "@types/node": "npm:*" "@ungap/structured-clone": "npm:^1.3.0" jest-util: "npm:30.0.5" merge-stream: "npm:^2.0.0" supports-color: "npm:^8.1.1" - checksum: 10c0/50a724b39b8691168a456544f32ef8e937c827cd6d326fa0bc27df786c80af1e1f16d9f2d9cc800af4baac85a0f9e9ed78fbd4a06f13eb32e72ec66d11b85f38 + checksum: 10c0/305a9c64d361e6be84e45d3b688da861569d43290a092ee05b8bc1e04fc5b3b8454423f14aa427902a5295487863fb857f7db79edbf2b9aca20874a94bc6f9a3 languageName: node linkType: hard @@ -37249,7 +37733,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:29.7.0, jest@npm:^29.6.4": +"jest@npm:29.7.0, jest@npm:^29.5.0, jest@npm:^29.6.4": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -37268,6 +37752,25 @@ __metadata: languageName: node linkType: hard +"jest@npm:^30.1.3": + version: 30.1.3 + resolution: "jest@npm:30.1.3" + dependencies: + "@jest/core": "npm:30.1.3" + "@jest/types": "npm:30.0.5" + import-local: "npm:^3.2.0" + jest-cli: "npm:30.1.3" + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: ./bin/jest.js + checksum: 10c0/6f0c18d8c94cfb3158cae90d048f38985b8aab9634f2fd8481f09ac0839697bf4a14fc3ed46d9d16e5bf653cad3af12547af62fbc46f64b3f06b32f8f8ee7d55 + languageName: node + linkType: hard + "jiti@npm:1.17.1": version: 1.17.1 resolution: "jiti@npm:1.17.1" @@ -38752,6 +39255,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^6.0.0": + version: 6.0.0 + resolution: "log-symbols@npm:6.0.0" + dependencies: + chalk: "npm:^5.3.0" + is-unicode-supported: "npm:^1.3.0" + checksum: 10c0/36636cacedba8f067d2deb4aad44e91a89d9efb3ead27e1846e7b82c9a10ea2e3a7bd6ce28a7ca616bebc60954ff25c67b0f92d20a6a746bb3cc52c3701891f6 + languageName: node + linkType: hard + "log-update@npm:^4.0.0": version: 4.0.0 resolution: "log-update@npm:4.0.0" @@ -41033,6 +41546,13 @@ __metadata: languageName: node linkType: hard +"mimic-function@npm:^5.0.0": + version: 5.0.1 + resolution: "mimic-function@npm:5.0.1" + checksum: 10c0/f3d9464dd1816ecf6bdf2aec6ba32c0728022039d992f178237d8e289b48764fee4131319e72eedd4f7f094e22ded0af836c3187a7edc4595d28dd74368fd81d + languageName: node + linkType: hard + "mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -41758,20 +42278,20 @@ __metadata: languageName: node linkType: hard -"nest-commander@npm:3.14.0": - version: 3.14.0 - resolution: "nest-commander@npm:3.14.0" +"nest-commander@npm:^3.19.1": + version: 3.19.1 + resolution: "nest-commander@npm:3.19.1" dependencies: "@fig/complete-commander": "npm:^3.0.0" - "@golevelup/nestjs-discovery": "npm:4.0.1" + "@golevelup/nestjs-discovery": "npm:4.0.3" commander: "npm:11.1.0" cosmiconfig: "npm:8.3.6" - inquirer: "npm:8.2.6" + inquirer: "npm:8.2.7" peerDependencies: - "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 - "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + "@nestjs/core": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 "@types/inquirer": ^8.1.3 - checksum: 10c0/3d5b448c36719dabdbbce3d8ef1a1a92b15189fc2e820e3bcb6f0085a4d6f00aeee27d494e9b23acd523b2730da36eb4c5b200726c80d1f54213cd82fa08fe9a + checksum: 10c0/36e24a41ab1b260940396713a3b23a7bbd2f40637f00ea4a209d7d574e75b539d8630689d6873fb73b80ed00cf5525b2207382c02f2e3369369403932554530e languageName: node linkType: hard @@ -42949,6 +43469,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^7.0.0": + version: 7.0.0 + resolution: "onetime@npm:7.0.0" + dependencies: + mimic-function: "npm:^5.0.0" + checksum: 10c0/5cb9179d74b63f52a196a2e7037ba2b9a893245a5532d3f44360012005c9cadb60851d56716ebff18a6f47129dab7168022445df47c2aff3b276d92585ed1221 + languageName: node + linkType: hard + "only@npm:~0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -43090,6 +43619,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:^8.0.0": + version: 8.2.0 + resolution: "ora@npm:8.2.0" + dependencies: + chalk: "npm:^5.3.0" + cli-cursor: "npm:^5.0.0" + cli-spinners: "npm:^2.9.2" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.0.0" + log-symbols: "npm:^6.0.0" + stdin-discarder: "npm:^0.2.2" + string-width: "npm:^7.2.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/7d9291255db22e293ea164f520b6042a3e906576ab06c9cf408bf9ef5664ba0a9f3bd258baa4ada058cfcc2163ef9b6696d51237a866682ce33295349ba02c3a + languageName: node + linkType: hard + "orderedmap@npm:^2.0.0": version: 2.1.1 resolution: "orderedmap@npm:2.1.1" @@ -44667,7 +45213,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: @@ -47224,6 +47770,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^5.0.0": + version: 5.1.0 + resolution: "restore-cursor@npm:5.1.0" + dependencies: + onetime: "npm:^7.0.0" + signal-exit: "npm:^4.1.0" + checksum: 10c0/c2ba89131eea791d1b25205bdfdc86699767e2b88dee2a590b1a6caa51737deac8bad0260a5ded2f7c074b7db2f3a626bcf1fcf3cdf35974cbeea5e2e6764f60 + languageName: node + linkType: hard + "restructure@npm:^3.0.0": version: 3.0.2 resolution: "restructure@npm:3.0.2" @@ -48975,6 +49531,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 10c0/c78375e82e956d7a64be6e63c809c7f058f5303efcaf62ea48350af072bacdb99c06cba39209b45a071c1acbd49116af30df1df9abb448df78a6005b72f10537 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -49229,6 +49792,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 + languageName: node + linkType: hard + "string.prototype.includes@npm:^2.0.1": version: 2.0.1 resolution: "string.prototype.includes@npm:2.0.1" @@ -50845,9 +51419,9 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.17.0, tsx@npm:^4.19.3": - version: 4.19.3 - resolution: "tsx@npm:4.19.3" +"tsx@npm:^4.17.0, tsx@npm:^4.19.3, tsx@npm:^4.7.0": + version: 4.20.5 + resolution: "tsx@npm:4.20.5" dependencies: esbuild: "npm:~0.25.0" fsevents: "npm:~2.3.3" @@ -50857,7 +51431,7 @@ __metadata: optional: true bin: tsx: dist/cli.mjs - checksum: 10c0/cacfb4cf1392ae10e8e4fe032ad26ccb07cd8a3b32e5a0da270d9c48d06ee74f743e4a84686cbc9d89b48032d59bbc56cd911e076f53cebe61dc24fa525ff790 + checksum: 10c0/70f9bf746be69281312a369c712902dbf9bcbdd9db9184a4859eb4859c36ef0c5a6d79b935c1ec429158ee73fd6584089400ae8790345dae34c5b0222bdb94f3 languageName: node linkType: hard @@ -50902,6 +51476,48 @@ __metadata: languageName: node linkType: hard +"twenty-apps@workspace:packages/twenty-apps": + version: 0.0.0-use.local + resolution: "twenty-apps@workspace:packages/twenty-apps" + dependencies: + "@swc/core": "npm:^1.9.3" + "@swc/jest": "npm:^0.2.36" + "@types/jest": "npm:^30.0.0" + "@types/node": "npm:^24.3.1" + jest: "npm:^30.1.3" + typescript: "npm:^5.9.2" + languageName: unknown + linkType: soft + +"twenty-cli@workspace:packages/twenty-cli": + version: 0.0.0-use.local + resolution: "twenty-cli@workspace:packages/twenty-cli" + dependencies: + "@types/fs-extra": "npm:^11.0.0" + "@types/inquirer": "npm:^9.0.0" + "@types/jest": "npm:^29.5.0" + "@types/node": "npm:^20.0.0" + ajv: "npm:^8.12.0" + ajv-formats: "npm:^2.1.1" + axios: "npm:^1.6.0" + chalk: "npm:^5.3.0" + chokidar: "npm:^4.0.0" + commander: "npm:^12.0.0" + dotenv: "npm:^16.4.0" + fs-extra: "npm:^11.2.0" + inquirer: "npm:^10.0.0" + jest: "npm:^29.5.0" + jsonc-parser: "npm:^3.2.0" + ora: "npm:^8.0.0" + tsx: "npm:^4.7.0" + typescript: "npm:^5.3.0" + yaml: "npm:^2.4.0" + zod: "npm:^3.22.0" + bin: + twenty: dist/cli.js + languageName: unknown + linkType: soft + "twenty-e2e-testing@workspace:packages/twenty-e2e-testing": version: 0.0.0-use.local resolution: "twenty-e2e-testing@workspace:packages/twenty-e2e-testing" @@ -51209,7 +51825,7 @@ __metadata: monaco-editor: "npm:^0.51.0" monaco-editor-auto-typings: "npm:^0.4.5" ms: "npm:2.1.3" - nest-commander: "npm:3.14.0" + nest-commander: "npm:^3.19.1" node-ical: "npm:^0.20.1" nodemailer: "npm:6.9.14" openapi-types: "npm:12.1.3" @@ -51426,8 +52042,10 @@ __metadata: "@types/chrome": "npm:^0.0.267" "@types/deep-equal": "npm:^1.0.1" "@types/express": "npm:^4.17.13" + "@types/fs-extra": "npm:^11.0.4" "@types/graphql-fields": "npm:^1.3.6" "@types/imapflow": "npm:^1.0.21" + "@types/inquirer": "npm:^9.0.9" "@types/jest": "npm:^30.0.0" "@types/lodash.camelcase": "npm:^4.3.7" "@types/lodash.compact": "npm:^3.0.9" @@ -51987,10 +52605,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 languageName: node linkType: hard @@ -52725,16 +53343,7 @@ __metadata: languageName: node linkType: hard -"use-debounce@npm:^10.0.0": - version: 10.0.2 - resolution: "use-debounce@npm:10.0.2" - peerDependencies: - react: ">=16.8.0" - checksum: 10c0/2d992108557a0ad3e59bc35028c0dbc6ad12088a08d992fd52aad3881dd24663606fe71f7fd925327fb98599c6252bfa4c143649351d48e6243d7a3332594fd6 - languageName: node - linkType: hard - -"use-debounce@npm:^10.0.4": +"use-debounce@npm:^10.0.0, use-debounce@npm:^10.0.4": version: 10.0.5 resolution: "use-debounce@npm:10.0.5" peerDependencies: @@ -54390,7 +54999,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.4.5, yaml@npm:^2.6.0": +"yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.4.0, yaml@npm:^2.4.5, yaml@npm:^2.6.0": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: @@ -54785,10 +55394,10 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.20.2, zod@npm:^3.23.8": - version: 3.24.2 - resolution: "zod@npm:3.24.2" - checksum: 10c0/c638c7220150847f13ad90635b3e7d0321b36cce36f3fc6050ed960689594c949c326dfe2c6fa87c14b126ee5d370ccdebd6efb304f41ef5557a4aaca2824565 +"zod@npm:^3.20.2, zod@npm:^3.22.0, zod@npm:^3.23.8": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c languageName: node linkType: hard