Compare commits

...

111 Commits

Author SHA1 Message Date
Nawaz Dhandala
d62816dd49 feat: update probe services to include environment variables for telemetry and logging 2026-02-18 13:50:28 +00:00
Nawaz Dhandala
7dd6129dad feat: add environment variables for log level and node environment in isolated-vm deployment 2026-02-18 13:42:14 +00:00
Nawaz Dhandala
7ccea02340 fix secret in probe 2026-02-18 13:37:22 +00:00
Nawaz Dhandala
0af41725b4 fix: add missing comma in dependencies section of package.json 2026-02-18 13:25:17 +00:00
Nawaz Dhandala
9f6bcddc1e feat: implement default notification rules for verified communication methods in User APIs 2026-02-18 13:16:54 +00:00
Nawaz Dhandala
97c461f7a3 fix: update key generation in MyOnCallPoliciesScreen to handle undefined policyId 2026-02-18 13:08:38 +00:00
Nawaz Dhandala
736f8bb83c chore: update adaptive and regular icons in MobileApp assets 2026-02-18 13:05:25 +00:00
Nawaz Dhandala
eb33daf64f refactor: add expo-splash-screen plugin configuration to app.json 2026-02-18 12:57:19 +00:00
Nawaz Dhandala
c3c90eef03 fix: ensure title and body are defaulted to empty strings in push notification 2026-02-18 12:25:14 +00:00
Nawaz Dhandala
e92e9f08d3 refactor: enhance push notification handling with PushNotificationService integration 2026-02-18 10:31:12 +00:00
Nawaz Dhandala
2b313a7702 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-18 09:58:01 +00:00
Nawaz Dhandala
3cf7c7d1ae refactor: implement push notification relay and enhance Expo integration 2026-02-18 09:56:10 +00:00
Nawaz Dhandala
76cfa7186e refactor: add eas.json configuration for build and submission settings 2026-02-18 09:00:13 +00:00
Simon Larsen
afaff717c0 Merge pull request #2304 from OneUptime/snyk-upgrade-950bdb1d48a0c3f367ba1c51cd0a7dee
[Snyk] Upgrade playwright from 1.57.0 to 1.58.0
2026-02-18 08:57:48 +00:00
Simon Larsen
fde0d5f2c6 Merge pull request #2302 from OneUptime/snyk-fix-68e2c2852a4c507876028cf50ec2c87c
[Snyk] Security upgrade nginx from 1.29.3-alpine to 1.29.5-alpine
2026-02-18 08:57:18 +00:00
Simon Larsen
d5c5387621 Merge branch 'master' into snyk-upgrade-950bdb1d48a0c3f367ba1c51cd0a7dee 2026-02-18 08:56:47 +00:00
Simon Larsen
e0ef6e9a77 Merge pull request #2308 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-18 08:56:11 +00:00
Nawaz Dhandala
2dc0dc4c96 refactor: add google services configuration for Firebase integration 2026-02-18 08:54:10 +00:00
Nawaz Dhandala
c9eb72ba2c refactor: enhance push notification registration with improved error handling and retry logic 2026-02-18 08:39:29 +00:00
Nawaz Dhandala
92e247d168 refactor: update logo size and styling in LoginScreen and ServerUrlScreen for improved visual consistency 2026-02-18 07:57:47 +00:00
Nawaz Dhandala
14988c438a refactor: update app.json slug and add permissions for biometric authentication 2026-02-18 07:48:31 +00:00
Nawaz Dhandala
d81682d02f Refactor styles in various components for improved readability and consistency
- Simplified style definitions in AlertCard, EpisodeCard, FeedTimeline, IncidentCard, AlertDetailScreen, AlertEpisodeDetailScreen, and others by using multi-line formatting.
- Enhanced the layout of components in HomeScreen, MyOnCallPoliciesScreen, and SettingsScreen for better maintainability.
- Updated text styles for better clarity and consistency across screens.
2026-02-18 07:47:07 +00:00
simlarsen
9d5faca3ec chore: npm audit fix 2026-02-18 02:31:53 +00:00
Nawaz Dhandala
89ccde1bc4 refactor: standardize device type strings to lowercase in registerPushDevice function for consistency 2026-02-17 22:12:43 +00:00
Nawaz Dhandala
3aab280dcd refactor: standardize shadow properties and background colors across components for improved consistency 2026-02-17 22:04:56 +00:00
Nawaz Dhandala
b8e44a1bcf refactor: update layout and styling in AddNoteModal and NotesSection for improved consistency and visual clarity 2026-02-17 21:55:20 +00:00
Nawaz Dhandala
4c3b4d23ff refactor: update assignment badge colors and icon in MyOnCallPoliciesScreen for improved visual consistency 2026-02-17 21:42:26 +00:00
Nawaz Dhandala
a4ff718d61 refactor: remove unnecessary shadow properties and simplify layout in MyOnCallPoliciesScreen for improved performance and consistency 2026-02-17 21:40:03 +00:00
Nawaz Dhandala
3433a815f3 refactor: simplify Pressable style and adjust layout properties in HomeScreen for improved accessibility and visual consistency 2026-02-17 21:32:54 +00:00
Nawaz Dhandala
2a20807126 refactor: update padding values in MyOnCallPoliciesScreen for improved layout consistency 2026-02-17 21:31:10 +00:00
Nawaz Dhandala
991dc1c842 refactor: update icon in AlertCard and IncidentCard from desktop-outline to pulse-outline for improved visual representation 2026-02-17 21:29:59 +00:00
Nawaz Dhandala
2026e7fd77 refactor: move marginBottom to parent View in EpisodeCard for improved layout consistency 2026-02-17 21:28:48 +00:00
Nawaz Dhandala
1d0016412e refactor: add marginBottom to SwipeableCard for improved layout spacing 2026-02-17 21:27:38 +00:00
Nawaz Dhandala
917f27fe11 Refactor styles in multiple screens to use inline styles instead of class names for consistency and improved readability. Updated layout properties and adjusted padding, margin, and font sizes across IncidentsScreen, MyOnCallPoliciesScreen, SettingsScreen, LoginScreen, and ServerUrlScreen for better UI alignment and responsiveness. 2026-02-17 21:25:03 +00:00
Nawaz Dhandala
c07c89e3dd refactor: replace Pressable with TouchableOpacity in StatCard for improved touch handling and update layout for better spacing 2026-02-17 21:16:58 +00:00
Nawaz Dhandala
32c4c1666d refactor: update button layout and styles in AlertEpisodeDetailScreen and IncidentEpisodeDetailScreen for consistency 2026-02-17 21:12:52 +00:00
Nawaz Dhandala
636a419cbd refactor: replace Pressable with TouchableOpacity for improved touch handling in SegmentedControl, AlertDetailScreen, and IncidentDetailScreen 2026-02-17 21:10:54 +00:00
Nawaz Dhandala
61699b9f4a refactor: replace Pressable with TouchableOpacity for theme selection options 2026-02-17 21:07:19 +00:00
Nawaz Dhandala
b6ed3643c3 fix: update splash screen background color and replace asset images 2026-02-17 20:57:17 +00:00
Nawaz Dhandala
9e73ac45a1 Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-17 15:08:20 +00:00
Nawaz Dhandala
7a3dbd0e8e refactor: streamline style definitions in multiple components for consistency 2026-02-17 14:57:46 +00:00
Nawaz Dhandala
1ec25c27ee refactor: replace TouchableOpacity with Pressable for improved touch handling across components 2026-02-17 14:55:36 +00:00
Simon Larsen
5286527155 Merge pull request #2305 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2026-02-17 09:49:52 +00:00
simlarsen
895af10755 chore: npm audit fix 2026-02-17 02:28:09 +00:00
Nawaz Dhandala
77ccca7e2a refactor: simplify query client setup and improve deep link handling in navigation 2026-02-16 22:31:13 +00:00
Nawaz Dhandala
66f46e9b84 chore: remove unused whois-json type definitions and update package dependencies 2026-02-16 20:06:11 +00:00
Nawaz Dhandala
91edae50b2 feat: Implement domain monitoring criteria and secret handling for domain names 2026-02-16 19:17:00 +00:00
Nawaz Dhandala
7ab3dfe043 feat: Add Domain Monitor functionality with WHOIS integration
- Updated package.json to include whois-json dependency.
- Created DomainMonitorCriteria class for evaluating domain monitoring criteria.
- Added DomainMonitorResponse interface to define the structure of domain monitoring responses.
- Introduced MonitorStepDomainMonitor interface and utility for managing domain monitor steps.
- Developed DomainMonitorStepForm component for user input on domain monitoring settings.
- Implemented DomainMonitorView component to display monitoring results and details.
- Added DomainMonitorUtil class for querying domain information using WHOIS data.
- Included parsing methods for name servers and domain status in DomainMonitorUtil.
2026-02-16 19:10:44 +00:00
Nawaz Dhandala
fb661126d4 chore: bump version to 9.5.13 2026-02-16 17:10:17 +00:00
Nawaz Dhandala
94c57f3189 style: update DNSSEC comment to use block comment format for clarity 2026-02-16 17:10:06 +00:00
Nawaz Dhandala
4de6021905 fix: update DNSSEC check to use a default resolver if none specified 2026-02-16 17:09:41 +00:00
Nawaz Dhandala
c62a49d499 fix: combine iputils and dnsutils installation in Dockerfile 2026-02-16 16:56:11 +00:00
snyk-bot
01fd5263ca fix: upgrade playwright from 1.57.0 to 1.58.0
Snyk has created this PR to upgrade playwright from 1.57.0 to 1.58.0.

See this package in npm:
playwright

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/49c81d9c-12c2-4e8e-b9e8-72f98b1b595c?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-02-16 15:58:33 +00:00
Nawaz Dhandala
d87eee68e8 style: refactor arrow functions to use explicit return statements for clarity 2026-02-16 14:55:08 +00:00
Nawaz Dhandala
3f4db5b7e0 feat: implement project creation restriction for non-admin users 2026-02-16 14:54:14 +00:00
Nawaz Dhandala
462ad9d6ab Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-16 14:36:46 +00:00
Nawaz Dhandala
6444d3d5cc Merge branch 'master' of https://github.com/OneUptime/oneuptime 2026-02-16 14:36:31 +00:00
Nawaz Dhandala
415222561b feat: add health checks and common dependencies for services in Docker Compose files 2026-02-16 14:34:26 +00:00
Nawaz Dhandala
8cf2661c63 style: adjust spacing and styling for CardSelect option groups and EvaluationLogList component 2026-02-16 11:23:21 +00:00
Nawaz Dhandala
a820f817ff fix: update header color for option groups in CardSelect component 2026-02-16 11:20:14 +00:00
Nawaz Dhandala
576927c6c7 refactor: rename monitoring category from "Active Monitoring" to "Basic Monitoring" 2026-02-16 11:08:57 +00:00
Nawaz Dhandala
e866db9e18 feat: enhance monitor type handling with categorized card select options and improve DNS resolver configuration 2026-02-16 11:08:31 +00:00
Nawaz Dhandala
8e91a786f9 refactor: streamline route definition for CLI endpoint 2026-02-16 10:39:43 +00:00
Nawaz Dhandala
02d16446f1 feat: update documentation links in CLI and MCP server pages for improved navigation 2026-02-16 10:32:28 +00:00
Nawaz Dhandala
5d5517258b Refactor code structure for improved readability and maintainability 2026-02-16 08:59:39 +00:00
Nawaz Dhandala
5df632c46c feat: update CLI documentation to reflect changes in incident state handling and queries 2026-02-16 08:47:01 +00:00
Nawaz Dhandala
c1ee79b339 feat: enhance CLI documentation with additional context and options for commands 2026-02-16 08:41:48 +00:00
Nawaz Dhandala
67265c0fc8 feat: include createdAt and planName in project details for improved project information 2026-02-16 08:32:46 +00:00
Nawaz Dhandala
72e5384012 chore: remove ora dependency from package.json and package-lock.json 2026-02-16 08:30:39 +00:00
Nawaz Dhandala
dc8e9d44b1 chore: bump version to 9.5.12 2026-02-16 08:21:14 +00:00
Nawaz Dhandala
91102ee952 feat: add CLI section to navigation with links to documentation 2026-02-15 20:29:36 +00:00
Nawaz Dhandala
e46d1ae7da chore: update compile workflow to install and compile Common package 2026-02-15 20:19:08 +00:00
Nawaz Dhandala
008005415a fix: update MarkdownContent styles to use ReturnType for better type inference 2026-02-15 20:14:56 +00:00
Nawaz Dhandala
c7362f3ada feat: add comprehensive CLI documentation including authentication, command reference, resource operations, output formats, and scripting for automation 2026-02-15 20:10:21 +00:00
Nawaz Dhandala
1f634576fe chore: update CLI to use npm package for Common, adjust TypeScript config, and add CI workflow
- Changed dependency for Common in CLI package.json to use npm package @oneuptime/common@latest.
- Updated tsconfig.json to exclude Tests, build, node_modules, and jest.config.json.
- Modified PublishAllPackages.sh to replace Common dependency with the pinned version during publish.
- Added GitHub Actions workflow for testing CLI on pull requests and pushes.
2026-02-15 12:05:35 +00:00
Nawaz Dhandala
d25a97fe17 Refactor components for improved readability and consistency
- Added missing newlines at the end of files in MarkdownContent.tsx and RootCauseCard.tsx
- Reformatted shadowColor and color properties in NotesSection.tsx, SegmentedControl.tsx, MainTabNavigator.tsx, HomeScreen.tsx for better readability
- Enhanced code formatting in SectionHeader.tsx and OnCallStackNavigator.tsx for consistency
- Improved readability of getEntityId function in useAllProjectOnCallPolicies.ts
- Refactored conditional rendering in AlertDetailScreen.tsx, AlertEpisodeDetailScreen.tsx, IncidentDetailScreen.tsx, and IncidentEpisodeDetailScreen.tsx for better clarity
2026-02-15 11:47:32 +00:00
Nawaz Dhandala
b89ff11db8 Add comprehensive tests for CLI commands and error handling
- Implement tests for ResourceCommands, ConfigCommands, UtilityCommands, and ErrorHandler.
- Enhance test coverage for command registration and execution, including list, get, create, update, delete, and count operations.
- Introduce tests for credential management and context handling in commands.
- Add error handling tests to ensure graceful exits on API errors and invalid inputs.
- Update jest configuration to exclude test files from coverage and adjust TypeScript settings.
2026-02-15 10:54:50 +00:00
Nawaz Dhandala
5ac5ffede5 feat(cli): initialize CLI package with TypeScript configuration and dependencies
- Added package.json for OneUptime CLI with scripts for development and build processes.
- Included TypeScript configuration (tsconfig.json) with strict type checking and module settings.
2026-02-15 10:36:30 +00:00
Nawaz Dhandala
d9167b89ba feat: Add toPlainText utility function and update components to use it for improved text handling 2026-02-14 22:02:21 +00:00
Nawaz Dhandala
66b995c64a feat: Implement On-Call policies feature with navigation, API integration, and UI components 2026-02-14 21:51:09 +00:00
Nawaz Dhandala
f383bbba4d refactor: Update notification icons in MainTabNavigator for improved clarity 2026-02-14 21:42:19 +00:00
Nawaz Dhandala
43f1a59042 refactor: Update icon names in HomeScreen for improved clarity and consistency 2026-02-14 21:41:10 +00:00
Nawaz Dhandala
7d49872edc refactor: Enhance HomeScreen layout by adding section headers for Incidents and Alerts, improving organization and readability 2026-02-14 21:38:03 +00:00
Nawaz Dhandala
6d2d5892b9 refactor: Update label from 'Total active issues' to 'Total active items' and adjust total count calculation in HomeScreen for improved accuracy 2026-02-14 21:35:53 +00:00
Nawaz Dhandala
756217e19e refactor: Simplify layout in HomeScreen by removing unnecessary Live indicator for cleaner UI 2026-02-14 21:34:57 +00:00
Nawaz Dhandala
ca3cf01be7 refactor: Update icon for Root Cause section in AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen for improved clarity 2026-02-14 21:33:33 +00:00
Nawaz Dhandala
fd0a81a0b1 refactor: Remove unused Ionicons import and clean up RootCauseCard layout for improved readability 2026-02-14 21:31:49 +00:00
Nawaz Dhandala
14016d188b refactor: Replace MarkdownContent with RootCauseCard component in AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen for improved root cause display 2026-02-14 21:31:30 +00:00
Nawaz Dhandala
3a2aff7f34 refactor: Integrate MarkdownContent component for improved markdown rendering in FeedTimeline, AlertDetailScreen, IncidentDetailScreen, and IncidentEpisodeDetailScreen; update package.json and package-lock.json for new dependencies 2026-02-14 21:28:04 +00:00
Nawaz Dhandala
4a6edfee06 refactor: Update AlertCard, EpisodeCard, and IncidentCard components for improved styling and consistency; enhance rgbToHex function for better color handling 2026-02-14 21:23:20 +00:00
Nawaz Dhandala
2dc1b8aa8c refactor: Enhance footer in SettingsScreen with updated layout, styling, and open source acknowledgment 2026-02-14 21:18:45 +00:00
Nawaz Dhandala
eb0f0e742d refactor: Update color handling in GradientButton, NotesSection, SegmentedControl, and SettingsScreen for improved theme support 2026-02-14 21:16:37 +00:00
Nawaz Dhandala
23c82c5239 refactor: Update UI components for improved styling and consistency across AlertCard, EpisodeCard, IncidentCard, GradientButton, NotesSection, SegmentedControl, and detail screens 2026-02-14 21:15:32 +00:00
Nawaz Dhandala
2b61e4f4b7 refactor: Improve layout and accessibility in AlertCard, EpisodeCard, and IncidentCard components 2026-02-14 21:11:58 +00:00
Nawaz Dhandala
9b21abf78d refactor: Enhance footer in SettingsScreen with gradient background and open source acknowledgment 2026-02-14 20:56:02 +00:00
Nawaz Dhandala
bd54b38a69 refactor: Update footer text in SettingsScreen for clarity and add text alignment 2026-02-14 20:55:26 +00:00
Nawaz Dhandala
4dc799d238 refactor: Add useWindowDimensions for responsive tab bar label visibility 2026-02-14 20:52:24 +00:00
Nawaz Dhandala
b4d90e3bef refactor: Update color palette to neutral monochrome accents for improved consistency 2026-02-14 20:51:14 +00:00
Nawaz Dhandala
6c8d4203da refactor: Update Logo component sizes in HomeScreen and SettingsScreen for improved layout 2026-02-14 20:50:19 +00:00
Nawaz Dhandala
f7e9745624 refactor: Update Logo component to use SvgXml and adjust sizes in HomeScreen and SettingsScreen 2026-02-14 20:48:00 +00:00
Nawaz Dhandala
f7d133adba refactor: Add TypeScript types for alert and incident models and update tsconfig paths 2026-02-14 20:38:15 +00:00
Nawaz Dhandala
b06c2cb1c3 refactor: Add root cause field to alert and incident APIs and update detail screens for display 2026-02-14 20:31:06 +00:00
Nawaz Dhandala
b51c5d9677 refactor: Improve UI components with enhanced styles and layout adjustments across multiple screens 2026-02-14 20:29:12 +00:00
Nawaz Dhandala
9a1e265d1c refactor: Enhance UI with LinearGradient backgrounds and improve component styles across multiple screens 2026-02-14 20:26:05 +00:00
Nawaz Dhandala
e18d75fc8e refactor: Add TypeScript declarations for additional languages in react-syntax-highlighter across multiple files 2026-02-14 20:14:11 +00:00
Nawaz Dhandala
5a68d2f726 refactor: Add TypeScript declarations for additional languages in react-syntax-highlighter 2026-02-14 20:03:04 +00:00
Nawaz Dhandala
dfa7c4875a refactor: Simplify JSX structure in multiple components for improved readability 2026-02-14 20:00:20 +00:00
Nawaz Dhandala
8a568e0495 chore: Bump version to 9.5.11 2026-02-14 19:59:46 +00:00
Nawaz Dhandala
7152058ee2 Merge branch 'dash-chunk' 2026-02-14 19:54:45 +00:00
snyk-bot
9dfbc05618 fix: Nginx/Dockerfile.tpl to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE322-LIBPNG-15062355
- https://snyk.io/vuln/SNYK-ALPINE322-LIBPNG-15062356
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121112
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121113
- https://snyk.io/vuln/SNYK-ALPINE322-OPENSSL-15121196
2026-02-14 14:53:26 +00:00
Nawaz Dhandala
e32d4395a3 refactor: update UI components for consistency and improved theming
- Refactored IncidentsScreen to use theme colors for backgrounds and text.
- Adjusted font sizes and styles across various components for better readability.
- Updated SettingsScreen to enhance layout and visual hierarchy, including removing GlassCard and using View components with theme colors.
- Modified LoginScreen and ServerUrlScreen to improve input field styling and overall layout.
- Revised color tokens in theme/colors.ts for better contrast and accessibility.
- Improved button labels for clarity and consistency.
2026-02-13 21:24:31 +00:00
178 changed files with 32484 additions and 3145 deletions

View File

@@ -398,6 +398,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install && npm run compile
- name: Compile MobileApp
uses: nick-fields/retry@v3
with:
@@ -420,4 +421,21 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: cd AIAgent && npm install && npm run compile && npm run dep-check
command: cd AIAgent && npm install && npm run compile && npm run dep-check
compile-cli:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- name: Compile CLI
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd CLI && npm install && npm run compile && npm run dep-check

21
.github/workflows/test.cli.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: CLI Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd CLI && npm install && npm run test

26
Accounts/index.d.ts vendored
View File

@@ -2,3 +2,29 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpg";
declare module "*.gif";
declare module "react-syntax-highlighter/dist/esm/prism-light";
declare module "react-syntax-highlighter/dist/esm/styles/prism";
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";

View File

@@ -2,3 +2,29 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpg";
declare module "*.gif";
declare module "react-syntax-highlighter/dist/esm/prism-light";
declare module "react-syntax-highlighter/dist/esm/styles/prism";
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";

View File

@@ -73,6 +73,46 @@ const Settings: FunctionComponent = (): ReactElement => {
modelId: ObjectID.getZeroObjectID(),
}}
/>
<CardModelDetail
name="Project Creation Settings"
cardProps={{
title: "Project Creation",
description:
"Control who can create new projects on this OneUptime Server.",
}}
isEditable={true}
editButtonText="Edit Settings"
formFields={[
{
field: {
disableUserProjectCreation: true,
},
title: "Restrict Project Creation to Admins Only",
fieldType: FormFieldSchemaType.Toggle,
required: false,
description:
"When enabled, only master admin users can create new projects.",
},
]}
modelDetailProps={{
modelType: GlobalConfig,
id: "model-detail-project-creation",
fields: [
{
field: {
disableUserProjectCreation: true,
},
fieldType: FieldType.Boolean,
title: "Restrict Project Creation to Admins Only",
placeholder: "No",
description:
"When enabled, only master admin users can create new projects.",
},
],
modelId: ObjectID.getZeroObjectID(),
}}
/>
</Page>
);
};

View File

@@ -0,0 +1,105 @@
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from "Common/Types/JSON";
import PushNotificationService from "Common/Server/Services/PushNotificationService";
const router: ExpressRouter = Express.getRouter();
// Simple in-memory rate limiter by IP
const rateLimitMap: Map<string, { count: number; resetTime: number }> =
new Map();
const RATE_LIMIT_WINDOW_MS: number = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX_REQUESTS: number = 60; // 60 requests per minute per IP
function isRateLimited(ip: string): boolean {
const now: number = Date.now();
const entry: { count: number; resetTime: number } | undefined =
rateLimitMap.get(ip);
if (!entry || now > entry.resetTime) {
rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
return false;
}
entry.count++;
return entry.count > RATE_LIMIT_MAX_REQUESTS;
}
// Clean up stale rate limit entries every 5 minutes
setInterval(() => {
const now: number = Date.now();
for (const [ip, entry] of rateLimitMap.entries()) {
if (now > entry.resetTime) {
rateLimitMap.delete(ip);
}
}
}, 5 * 60 * 1000);
router.post(
"/send",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const clientIp: string =
(req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() ||
req.socket.remoteAddress ||
"unknown";
if (isRateLimited(clientIp)) {
res.status(429).json({
message: "Rate limit exceeded. Please try again later.",
});
return;
}
if (!PushNotificationService.hasExpoAccessToken()) {
throw new BadDataException(
"Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
);
}
const body: JSONObject = req.body as JSONObject;
const to: string | undefined = body["to"] as string | undefined;
if (!to || !PushNotificationService.isValidExpoPushToken(to)) {
throw new BadDataException(
"Invalid or missing push token. Must be a valid Expo push token.",
);
}
const title: string | undefined = body["title"] as string | undefined;
const messageBody: string | undefined = body["body"] as
| string
| undefined;
if (!title && !messageBody) {
throw new BadDataException(
"At least one of 'title' or 'body' must be provided.",
);
}
await PushNotificationService.sendRelayPushNotification({
to: to,
title: title,
body: messageBody,
data: (body["data"] as { [key: string]: string }) || {},
sound: (body["sound"] as string) || "default",
priority: (body["priority"] as string) || "high",
channelId: (body["channelId"] as string) || "default",
});
return Response.sendJsonObjectResponse(req, res, { success: true });
} catch (err) {
return next(err);
}
},
);
export default router;

View File

@@ -4,6 +4,7 @@ import MailAPI from "./API/Mail";
import SmsAPI from "./API/SMS";
import WhatsAppAPI from "./API/WhatsApp";
import PushNotificationAPI from "./API/PushNotification";
import PushRelayAPI from "./API/PushRelay";
import SMTPConfigAPI from "./API/SMTPConfig";
import PhoneNumberAPI from "./API/PhoneNumber";
import IncomingCallAPI from "./API/IncomingCall";
@@ -21,6 +22,7 @@ const NotificationFeatureSet: FeatureSet = {
app.use([`/${APP_NAME}/sms`, "/sms"], SmsAPI);
app.use([`/${APP_NAME}/whatsapp`, "/whatsapp"], WhatsAppAPI);
app.use([`/${APP_NAME}/push`, "/push"], PushNotificationAPI);
app.use([`/${APP_NAME}/push-relay`, "/push-relay"], PushRelayAPI);
app.use([`/${APP_NAME}/call`, "/call"], CallAPI);
app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI);
app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI);

58
App/package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@sendgrid/mail": "^8.1.0",
"Common": "file:../Common",
"ejs": "^3.1.9",
"expo-server-sdk": "^5.0.0",
"handlebars": "^4.7.8",
"nodemailer": "^6.9.7",
"ts-node": "^10.9.1",
@@ -2166,6 +2167,12 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/err-code": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -2299,6 +2306,20 @@
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
}
},
"node_modules/expo-server-sdk": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-5.0.0.tgz",
"integrity": "sha512-GEp1XYLU80iS/hdRo3c2n092E8TgTXcHSuw6Lw68dSoWaAgiLPI2R+e5hp5+hGF1TtJZOi2nxtJX63+XA3iz9g==",
"license": "MIT",
"dependencies": {
"promise-limit": "^2.7.0",
"promise-retry": "^2.0.1",
"undici": "^7.2.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -4167,6 +4188,25 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/promise-limit": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
"license": "ISC"
},
"node_modules/promise-retry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
"license": "MIT",
"dependencies": {
"err-code": "^2.0.2",
"retry": "^0.12.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -4290,6 +4330,15 @@
"node": ">=10"
}
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -4801,6 +4850,15 @@
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true
},
"node_modules/undici": {
"version": "7.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",

View File

@@ -23,6 +23,7 @@
"@sendgrid/mail": "^8.1.0",
"Common": "file:../Common",
"ejs": "^3.1.9",
"expo-server-sdk": "^5.0.0",
"handlebars": "^4.7.8",
"nodemailer": "^6.9.7",
"ts-node": "^10.9.1",

View File

@@ -0,0 +1,140 @@
import { Command } from "commander";
import * as ConfigManager from "../Core/ConfigManager";
import { CLIContext } from "../Types/CLITypes";
import { printSuccess, printError, printInfo } from "../Core/OutputFormatter";
import Table from "cli-table3";
import chalk from "chalk";
export function registerConfigCommands(program: Command): void {
// Login command
const loginCmd: Command = program
.command("login")
.description("Authenticate with a OneUptime instance")
.argument("<api-key>", "API key for authentication")
.argument(
"<instance-url>",
"OneUptime instance URL (e.g. https://oneuptime.com)",
)
.option("--context-name <name>", "Name for this context", "default")
.action(
(
apiKey: string,
instanceUrl: string,
options: { contextName: string },
) => {
try {
const context: CLIContext = {
name: options.contextName,
apiUrl: instanceUrl.replace(/\/+$/, ""),
apiKey: apiKey,
};
ConfigManager.addContext(context);
ConfigManager.setCurrentContext(context.name);
printSuccess(
`Logged in successfully. Context "${context.name}" is now active.`,
);
} catch (error) {
printError(
`Login failed: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
},
);
// Suppress unused variable warning - loginCmd is used for registration
void loginCmd;
// Context commands
const contextCmd: Command = program
.command("context")
.description("Manage CLI contexts (environments/projects)");
contextCmd
.command("list")
.description("List all configured contexts")
.action(() => {
const contexts: Array<CLIContext & { isCurrent: boolean }> =
ConfigManager.listContexts();
if (contexts.length === 0) {
printInfo(
"No contexts configured. Run `oneuptime login` to create one.",
);
return;
}
const noColor: boolean =
process.env["NO_COLOR"] !== undefined ||
process.argv.includes("--no-color");
const table: Table.Table = new Table({
head: ["", "Name", "URL"].map((h: string) => {
return noColor ? h : chalk.cyan(h);
}),
style: { head: [], border: [] },
});
for (const ctx of contexts) {
table.push([ctx.isCurrent ? "*" : "", ctx.name, ctx.apiUrl]);
}
// eslint-disable-next-line no-console
console.log(table.toString());
});
contextCmd
.command("use <name>")
.description("Switch to a different context")
.action((name: string) => {
try {
ConfigManager.setCurrentContext(name);
printSuccess(`Switched to context "${name}".`);
} catch (error) {
printError(error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
contextCmd
.command("current")
.description("Show the current active context")
.action(() => {
const ctx: CLIContext | null = ConfigManager.getCurrentContext();
if (!ctx) {
printInfo(
"No current context set. Run `oneuptime login` to create one.",
);
return;
}
const maskedKey: string =
ctx.apiKey.length > 8
? ctx.apiKey.substring(0, 4) +
"****" +
ctx.apiKey.substring(ctx.apiKey.length - 4)
: "****";
// eslint-disable-next-line no-console
console.log(`Context: ${ctx.name}`);
// eslint-disable-next-line no-console
console.log(`URL: ${ctx.apiUrl}`);
// eslint-disable-next-line no-console
console.log(`API Key: ${maskedKey}`);
});
contextCmd
.command("delete <name>")
.description("Delete a context")
.action((name: string) => {
try {
ConfigManager.removeContext(name);
printSuccess(`Context "${name}" deleted.`);
} catch (error) {
printError(error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
}

View File

@@ -0,0 +1,356 @@
import { Command } from "commander";
import DatabaseModels from "Common/Models/DatabaseModels/Index";
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { ResourceInfo, ResolvedCredentials } from "../Types/CLITypes";
import { executeApiRequest, ApiOperation } from "../Core/ApiClient";
import { CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
import { formatOutput, printSuccess } from "../Core/OutputFormatter";
import { handleError } from "../Core/ErrorHandler";
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
import { JSONObject, JSONValue } from "Common/Types/JSON";
import * as fs from "fs";
function toKebabCase(str: string): string {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/[\s_]+/g, "-")
.toLowerCase();
}
function parseJsonArg(value: string): JSONObject {
try {
return JSON.parse(value) as JSONObject;
} catch {
throw new Error(`Invalid JSON: ${value}`);
}
}
export function discoverResources(): ResourceInfo[] {
const resources: ResourceInfo[] = [];
// Database models
for (const ModelClass of DatabaseModels) {
try {
const model: BaseModel = new ModelClass();
const tableName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || tableName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
if (tableName && model.enableMCP && apiPath) {
resources.push({
name: toKebabCase(singularName),
singularName,
pluralName,
apiPath,
tableName,
modelType: "database",
});
}
} catch {
// Skip models that fail to instantiate
}
}
// Analytics models
for (const ModelClass of AnalyticsModels) {
try {
const model: AnalyticsBaseModel = new ModelClass();
const tableName: string = model.tableName || ModelClass.name;
const singularName: string = model.singularName || tableName;
const pluralName: string = model.pluralName || `${singularName}s`;
const apiPath: string | undefined = model.crudApiPath?.toString();
if (tableName && model.enableMCP && apiPath) {
resources.push({
name: toKebabCase(singularName),
singularName,
pluralName,
apiPath,
tableName,
modelType: "analytics",
});
}
} catch {
// Skip models that fail to instantiate
}
}
return resources;
}
function getParentOptions(cmd: Command): CLIOptions {
// Walk up to root program to get global options
let current: Command | null = cmd;
while (current?.parent) {
current = current.parent;
}
const opts: Record<string, unknown> = current?.opts() || {};
return {
apiKey: opts["apiKey"] as string | undefined,
url: opts["url"] as string | undefined,
context: opts["context"] as string | undefined,
};
}
function registerListCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("list")
.description(`List ${resource.pluralName}`)
.option("--query <json>", "Filter query as JSON")
.option("--limit <n>", "Max results to return", "10")
.option("--skip <n>", "Number of results to skip", "0")
.option("--sort <json>", "Sort order as JSON")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(
async (options: {
query?: string;
limit: string;
skip: string;
sort?: string;
output?: string;
}) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const select: JSONObject = generateAllFieldsSelect(
resource.tableName,
resource.modelType,
);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "list" as ApiOperation,
query: options.query ? parseJsonArg(options.query) : undefined,
select,
skip: parseInt(options.skip, 10),
limit: parseInt(options.limit, 10),
sort: options.sort ? parseJsonArg(options.sort) : undefined,
});
// Extract data array from response
const responseData: JSONValue =
result && typeof result === "object" && !Array.isArray(result)
? ((result as JSONObject)["data"] as JSONValue) || result
: result;
// eslint-disable-next-line no-console
console.log(formatOutput(responseData, options.output));
} catch (error) {
handleError(error);
}
},
);
}
function registerGetCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("get <id>")
.description(`Get a single ${resource.singularName} by ID`)
.option("-o, --output <format>", "Output format: json, table, wide")
.action(async (id: string, options: { output?: string }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const select: JSONObject = generateAllFieldsSelect(
resource.tableName,
resource.modelType,
);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "read" as ApiOperation,
id,
select,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
});
}
function registerCreateCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("create")
.description(`Create a new ${resource.singularName}`)
.option("--data <json>", "Resource data as JSON")
.option("--file <path>", "Read resource data from a JSON file")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(
async (options: { data?: string; file?: string; output?: string }) => {
try {
let data: JSONObject;
if (options.file) {
const fileContent: string = fs.readFileSync(options.file, "utf-8");
data = JSON.parse(fileContent) as JSONObject;
} else if (options.data) {
data = parseJsonArg(options.data);
} else {
throw new Error("Either --data or --file is required for create.");
}
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "create" as ApiOperation,
data,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
},
);
}
function registerUpdateCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("update <id>")
.description(`Update an existing ${resource.singularName}`)
.requiredOption("--data <json>", "Fields to update as JSON")
.option("-o, --output <format>", "Output format: json, table, wide")
.action(async (id: string, options: { data: string; output?: string }) => {
try {
const data: JSONObject = parseJsonArg(options.data);
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "update" as ApiOperation,
id,
data,
});
// eslint-disable-next-line no-console
console.log(formatOutput(result, options.output));
} catch (error) {
handleError(error);
}
});
}
function registerDeleteCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("delete <id>")
.description(`Delete a ${resource.singularName}`)
.option("--force", "Skip confirmation")
.action(async (id: string, _options: { force?: boolean }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "delete" as ApiOperation,
id,
});
printSuccess(`${resource.singularName} ${id} deleted successfully.`);
} catch (error) {
handleError(error);
}
});
}
function registerCountCommand(
resourceCmd: Command,
resource: ResourceInfo,
): void {
resourceCmd
.command("count")
.description(`Count ${resource.pluralName}`)
.option("--query <json>", "Filter query as JSON")
.action(async (options: { query?: string }) => {
try {
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
const result: JSONValue = await executeApiRequest({
apiUrl: creds.apiUrl,
apiKey: creds.apiKey,
apiPath: resource.apiPath,
operation: "count" as ApiOperation,
query: options.query ? parseJsonArg(options.query) : undefined,
});
// Count response is typically { count: number }
if (
result &&
typeof result === "object" &&
!Array.isArray(result) &&
"count" in (result as JSONObject)
) {
// eslint-disable-next-line no-console
console.log((result as JSONObject)["count"]);
} else {
// eslint-disable-next-line no-console
console.log(result);
}
} catch (error) {
handleError(error);
}
});
}
export function registerResourceCommands(program: Command): void {
const resources: ResourceInfo[] = discoverResources();
for (const resource of resources) {
const resourceCmd: Command = program
.command(resource.name)
.description(`Manage ${resource.pluralName} (${resource.modelType})`);
// Database models get full CRUD
if (resource.modelType === "database") {
registerListCommand(resourceCmd, resource);
registerGetCommand(resourceCmd, resource);
registerCreateCommand(resourceCmd, resource);
registerUpdateCommand(resourceCmd, resource);
registerDeleteCommand(resourceCmd, resource);
registerCountCommand(resourceCmd, resource);
}
// Analytics models get create, list, count
if (resource.modelType === "analytics") {
registerListCommand(resourceCmd, resource);
registerCreateCommand(resourceCmd, resource);
registerCountCommand(resourceCmd, resource);
}
}
}

View File

@@ -0,0 +1,129 @@
import { Command } from "commander";
import {
CLIContext,
ResolvedCredentials,
ResourceInfo,
} from "../Types/CLITypes";
import {
getCurrentContext,
CLIOptions,
getResolvedCredentials,
} from "../Core/ConfigManager";
import { printInfo, printError } from "../Core/OutputFormatter";
import { discoverResources } from "./ResourceCommands";
import Table from "cli-table3";
import chalk from "chalk";
export function registerUtilityCommands(program: Command): void {
// Version command
program
.command("version")
.description("Print CLI version")
.action(() => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
const pkg: { version: string } = require("../package.json") as {
version: string;
};
// eslint-disable-next-line no-console
console.log(pkg.version);
} catch {
// Fallback if package.json can't be loaded at runtime
// eslint-disable-next-line no-console
console.log("1.0.0");
}
});
// Whoami command
program
.command("whoami")
.description("Show current authentication info")
.action(() => {
try {
const ctx: CLIContext | null = getCurrentContext();
const opts: Record<string, unknown> = program.opts();
const cliOpts: CLIOptions = {
apiKey: opts["apiKey"] as string | undefined,
url: opts["url"] as string | undefined,
context: opts["context"] as string | undefined,
};
let creds: ResolvedCredentials;
try {
creds = getResolvedCredentials(cliOpts);
} catch {
printInfo(
"Not authenticated. Run `oneuptime login` to authenticate.",
);
return;
}
const maskedKey: string =
creds.apiKey.length > 8
? creds.apiKey.substring(0, 4) +
"****" +
creds.apiKey.substring(creds.apiKey.length - 4)
: "****";
// eslint-disable-next-line no-console
console.log(`URL: ${creds.apiUrl}`);
// eslint-disable-next-line no-console
console.log(`API Key: ${maskedKey}`);
if (ctx) {
// eslint-disable-next-line no-console
console.log(`Context: ${ctx.name}`);
}
} catch (error) {
printError(error instanceof Error ? error.message : String(error));
process.exit(1);
}
});
// Resources command
program
.command("resources")
.description("List all available resource types")
.option("--type <type>", "Filter by model type: database, analytics")
.action((options: { type?: string }) => {
const resources: ResourceInfo[] = discoverResources();
const filtered: ResourceInfo[] = options.type
? resources.filter((r: ResourceInfo) => {
return r.modelType === options.type;
})
: resources;
if (filtered.length === 0) {
printInfo("No resources found.");
return;
}
const noColor: boolean =
process.env["NO_COLOR"] !== undefined ||
process.argv.includes("--no-color");
const table: Table.Table = new Table({
head: ["Command", "Singular", "Plural", "Type", "API Path"].map(
(h: string) => {
return noColor ? h : chalk.cyan(h);
},
),
style: { head: [], border: [] },
});
for (const r of filtered) {
table.push([
r.name,
r.singularName,
r.pluralName,
r.modelType,
r.apiPath,
]);
}
// eslint-disable-next-line no-console
console.log(table.toString());
// eslint-disable-next-line no-console
console.log(`\nTotal: ${filtered.length} resources`);
});
}

150
CLI/Core/ApiClient.ts Normal file
View File

@@ -0,0 +1,150 @@
import API from "Common/Utils/API";
import URL from "Common/Types/API/URL";
import Route from "Common/Types/API/Route";
import Headers from "Common/Types/API/Headers";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import Protocol from "Common/Types/API/Protocol";
import Hostname from "Common/Types/API/Hostname";
import { JSONObject, JSONValue } from "Common/Types/JSON";
export type ApiOperation =
| "create"
| "read"
| "list"
| "update"
| "delete"
| "count";
export interface ApiRequestOptions {
apiUrl: string;
apiKey: string;
apiPath: string;
operation: ApiOperation;
id?: string | undefined;
data?: JSONObject | undefined;
query?: JSONObject | undefined;
select?: JSONObject | undefined;
skip?: number | undefined;
limit?: number | undefined;
sort?: JSONObject | undefined;
}
function buildApiRoute(
apiPath: string,
operation: ApiOperation,
id?: string,
): Route {
let fullPath: string = `/api${apiPath}`;
switch (operation) {
case "read":
if (id) {
fullPath = `/api${apiPath}/${id}/get-item`;
}
break;
case "update":
case "delete":
if (id) {
fullPath = `/api${apiPath}/${id}/`;
}
break;
case "count":
fullPath = `/api${apiPath}/count`;
break;
case "list":
fullPath = `/api${apiPath}/get-list`;
break;
case "create":
default:
fullPath = `/api${apiPath}`;
break;
}
return new Route(fullPath);
}
function buildHeaders(apiKey: string): Headers {
return {
"Content-Type": "application/json",
Accept: "application/json",
APIKey: apiKey,
};
}
function buildRequestData(options: ApiRequestOptions): JSONObject | undefined {
switch (options.operation) {
case "create":
return { data: options.data || {} } as JSONObject;
case "update":
return { data: options.data || {} } as JSONObject;
case "list":
case "count":
return {
query: options.query || {},
select: options.select || {},
skip: options.skip || 0,
limit: options.limit || 10,
sort: options.sort || {},
} as JSONObject;
case "read":
return {
select: options.select || {},
} as JSONObject;
case "delete":
default:
return undefined;
}
}
export async function executeApiRequest(
options: ApiRequestOptions,
): Promise<JSONValue> {
const url: URL = URL.fromString(options.apiUrl);
const protocol: Protocol = url.protocol;
const hostname: Hostname = url.hostname;
const api: API = new API(protocol, hostname, new Route("/"));
const route: Route = buildApiRoute(
options.apiPath,
options.operation,
options.id,
);
const headers: Headers = buildHeaders(options.apiKey);
const data: JSONObject | undefined = buildRequestData(options);
const requestUrl: URL = new URL(api.protocol, api.hostname, route);
const baseOptions: { url: URL; headers: Headers } = {
url: requestUrl,
headers,
};
let response: HTTPResponse<JSONObject> | HTTPErrorResponse;
switch (options.operation) {
case "create":
case "count":
case "list":
case "read":
response = await API.post(data ? { ...baseOptions, data } : baseOptions);
break;
case "update":
response = await API.put(data ? { ...baseOptions, data } : baseOptions);
break;
case "delete":
response = await API.delete(
data ? { ...baseOptions, data } : baseOptions,
);
break;
default:
throw new Error(`Unsupported operation: ${options.operation}`);
}
if (response instanceof HTTPErrorResponse) {
throw new Error(
`API error (${response.statusCode}): ${response.message || "Unknown error"}`,
);
}
return response.data;
}

141
CLI/Core/ConfigManager.ts Normal file
View File

@@ -0,0 +1,141 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { CLIConfig, CLIContext, ResolvedCredentials } from "../Types/CLITypes";
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
function getDefaultConfig(): CLIConfig {
return {
currentContext: "",
contexts: {},
defaults: {
output: "table",
limit: 10,
},
};
}
export function load(): CLIConfig {
try {
if (!fs.existsSync(CONFIG_FILE)) {
return getDefaultConfig();
}
const raw: string = fs.readFileSync(CONFIG_FILE, "utf-8");
return JSON.parse(raw) as CLIConfig;
} catch {
return getDefaultConfig();
}
}
export function save(config: CLIConfig): void {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
mode: 0o600,
});
}
export function getCurrentContext(): CLIContext | null {
const config: CLIConfig = load();
if (!config.currentContext) {
return null;
}
return config.contexts[config.currentContext] || null;
}
export function setCurrentContext(name: string): void {
const config: CLIConfig = load();
if (!config.contexts[name]) {
throw new Error(`Context "${name}" does not exist.`);
}
config.currentContext = name;
save(config);
}
export function addContext(context: CLIContext): void {
const config: CLIConfig = load();
config.contexts[context.name] = context;
if (!config.currentContext) {
config.currentContext = context.name;
}
save(config);
}
export function removeContext(name: string): void {
const config: CLIConfig = load();
if (!config.contexts[name]) {
throw new Error(`Context "${name}" does not exist.`);
}
delete config.contexts[name];
if (config.currentContext === name) {
const remaining: string[] = Object.keys(config.contexts);
config.currentContext = remaining[0] || "";
}
save(config);
}
export function listContexts(): Array<CLIContext & { isCurrent: boolean }> {
const config: CLIConfig = load();
return Object.values(config.contexts).map(
(ctx: CLIContext): CLIContext & { isCurrent: boolean } => {
return {
...ctx,
isCurrent: ctx.name === config.currentContext,
};
},
);
}
export interface CLIOptions {
apiKey?: string | undefined;
url?: string | undefined;
context?: string | undefined;
}
export function getResolvedCredentials(
cliOptions: CLIOptions,
): ResolvedCredentials {
// Priority 1: CLI flags
if (cliOptions.apiKey && cliOptions.url) {
return { apiKey: cliOptions.apiKey, apiUrl: cliOptions.url };
}
// Priority 2: Environment variables
const envApiKey: string | undefined = process.env["ONEUPTIME_API_KEY"];
const envUrl: string | undefined = process.env["ONEUPTIME_URL"];
if (envApiKey && envUrl) {
return { apiKey: envApiKey, apiUrl: envUrl };
}
// Priority 3: Specific context if specified via --context flag
if (cliOptions.context) {
const config: CLIConfig = load();
const ctx: CLIContext | undefined = config.contexts[cliOptions.context];
if (ctx) {
return { apiKey: ctx.apiKey, apiUrl: ctx.apiUrl };
}
throw new Error(`Context "${cliOptions.context}" does not exist.`);
}
// Priority 4: Current context in config file
const currentCtx: CLIContext | null = getCurrentContext();
if (currentCtx) {
return { apiKey: currentCtx.apiKey, apiUrl: currentCtx.apiUrl };
}
// Partial env vars + partial context
if (envApiKey || envUrl) {
const ctx: CLIContext | null = getCurrentContext();
return {
apiKey: envApiKey || ctx?.apiKey || "",
apiUrl: envUrl || ctx?.apiUrl || "",
};
}
throw new Error(
"No credentials found. Run `oneuptime login` or set ONEUPTIME_API_KEY and ONEUPTIME_URL environment variables.",
);
}

43
CLI/Core/ErrorHandler.ts Normal file
View File

@@ -0,0 +1,43 @@
import { printError } from "./OutputFormatter";
export enum ExitCode {
Success = 0,
GeneralError = 1,
AuthError = 2,
NotFound = 3,
}
export function handleError(error: unknown): never {
if (error instanceof Error) {
const message: string = error.message;
// Check for auth-related errors
if (
message.includes("API key") ||
message.includes("credentials") ||
message.includes("Unauthorized") ||
message.includes("401")
) {
printError(`Authentication error: ${message}`);
process.exit(ExitCode.AuthError);
}
// Check for not found errors
if (message.includes("404") || message.includes("not found")) {
printError(`Not found: ${message}`);
process.exit(ExitCode.NotFound);
}
// General API errors
if (message.includes("API error")) {
printError(message);
process.exit(ExitCode.GeneralError);
}
printError(`Error: ${message}`);
} else {
printError(`Error: ${String(error)}`);
}
process.exit(ExitCode.GeneralError);
}

195
CLI/Core/OutputFormatter.ts Normal file
View File

@@ -0,0 +1,195 @@
import { OutputFormat } from "../Types/CLITypes";
import { JSONValue, JSONObject, JSONArray } from "Common/Types/JSON";
import Table from "cli-table3";
import chalk from "chalk";
function isColorDisabled(): boolean {
return (
process.env["NO_COLOR"] !== undefined || process.argv.includes("--no-color")
);
}
function detectOutputFormat(cliFormat?: string): OutputFormat {
if (cliFormat) {
if (cliFormat === "json") {
return OutputFormat.JSON;
}
if (cliFormat === "wide") {
return OutputFormat.Wide;
}
if (cliFormat === "table") {
return OutputFormat.Table;
}
}
// If stdout is not a TTY (piped), default to JSON
if (!process.stdout.isTTY) {
return OutputFormat.JSON;
}
return OutputFormat.Table;
}
function formatJson(data: JSONValue): string {
return JSON.stringify(data, null, 2);
}
function formatTable(data: JSONValue, wide: boolean): string {
if (!data) {
return "No data returned.";
}
// Handle single object
if (!Array.isArray(data)) {
return formatSingleObject(data as JSONObject);
}
const items: JSONArray = data as JSONArray;
if (items.length === 0) {
return "No results found.";
}
// Get all keys from the first item
const firstItem: JSONObject = items[0] as JSONObject;
if (!firstItem || typeof firstItem !== "object") {
return formatJson(data);
}
let columns: string[] = Object.keys(firstItem);
// In non-wide mode, limit columns to keep the table readable
if (!wide && columns.length > 6) {
// Prioritize common fields
const priority: string[] = [
"_id",
"name",
"title",
"status",
"createdAt",
"updatedAt",
];
const prioritized: string[] = priority.filter((col: string) => {
return columns.includes(col);
});
const remaining: string[] = columns.filter((col: string) => {
return !priority.includes(col);
});
columns = [...prioritized, ...remaining].slice(0, 6);
}
const useColor: boolean = !isColorDisabled();
const table: Table.Table = new Table({
head: columns.map((col: string) => {
return useColor ? chalk.cyan(col) : col;
}),
style: {
head: [],
border: [],
},
wordWrap: true,
});
for (const item of items) {
const row: string[] = columns.map((col: string) => {
const val: JSONValue = (item as JSONObject)[col] as JSONValue;
return truncateValue(val);
});
table.push(row);
}
return table.toString();
}
function formatSingleObject(obj: JSONObject): string {
const useColor: boolean = !isColorDisabled();
const table: Table.Table = new Table({
style: { head: [], border: [] },
});
for (const [key, value] of Object.entries(obj)) {
const label: string = useColor ? chalk.cyan(key) : key;
table.push({ [label]: truncateValue(value as JSONValue) });
}
return table.toString();
}
function truncateValue(val: JSONValue, maxLen: number = 60): string {
if (val === null || val === undefined) {
return "";
}
if (typeof val === "object") {
const str: string = JSON.stringify(val);
if (str.length > maxLen) {
return str.substring(0, maxLen - 3) + "...";
}
return str;
}
const str: string = String(val);
if (str.length > maxLen) {
return str.substring(0, maxLen - 3) + "...";
}
return str;
}
export function formatOutput(data: JSONValue, format?: string): string {
const outputFormat: OutputFormat = detectOutputFormat(format);
switch (outputFormat) {
case OutputFormat.JSON:
return formatJson(data);
case OutputFormat.Wide:
return formatTable(data, true);
case OutputFormat.Table:
default:
return formatTable(data, false);
}
}
export function printSuccess(message: string): void {
const useColor: boolean = !isColorDisabled();
if (useColor) {
// eslint-disable-next-line no-console
console.log(chalk.green(message));
} else {
// eslint-disable-next-line no-console
console.log(message);
}
}
export function printError(message: string): void {
const useColor: boolean = !isColorDisabled();
if (useColor) {
// eslint-disable-next-line no-console
console.error(chalk.red(message));
} else {
// eslint-disable-next-line no-console
console.error(message);
}
}
export function printWarning(message: string): void {
const useColor: boolean = !isColorDisabled();
if (useColor) {
// eslint-disable-next-line no-console
console.error(chalk.yellow(message));
} else {
// eslint-disable-next-line no-console
console.error(message);
}
}
export function printInfo(message: string): void {
const useColor: boolean = !isColorDisabled();
if (useColor) {
// eslint-disable-next-line no-console
console.log(chalk.blue(message));
} else {
// eslint-disable-next-line no-console
console.log(message);
}
}

27
CLI/Index.ts Normal file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env npx ts-node
import { Command } from "commander";
import { registerConfigCommands } from "./Commands/ConfigCommands";
import { registerResourceCommands } from "./Commands/ResourceCommands";
import { registerUtilityCommands } from "./Commands/UtilityCommands";
const program: Command = new Command();
program
.name("oneuptime")
.description(
"OneUptime CLI - Manage your OneUptime resources from the command line",
)
.version("1.0.0")
.option("--api-key <key>", "API key (overrides config)")
.option("--url <url>", "OneUptime instance URL (overrides config)")
.option("--context <name>", "Use a specific context")
.option("-o, --output <format>", "Output format: json, table, wide")
.option("--no-color", "Disable colored output");
// Register command groups
registerConfigCommands(program);
registerUtilityCommands(program);
registerResourceCommands(program);
program.parse(process.argv);

220
CLI/README.md Normal file
View File

@@ -0,0 +1,220 @@
# @oneuptime/cli
Command-line interface for managing OneUptime resources. Supports all MCP-enabled resources with full CRUD operations, named contexts for multiple environments, and flexible output formats.
## Installation
```bash
npm install -g @oneuptime/cli
```
Or run directly within the monorepo:
```bash
cd CLI
npm install
npm start -- --help
```
## Quick Start
```bash
# Authenticate with your OneUptime instance
oneuptime login <api-key> <instance-url>
oneuptime login sk-your-api-key https://oneuptime.com
# List incidents
oneuptime incident list --limit 10
# Get a single resource by ID
oneuptime monitor get 550e8400-e29b-41d4-a716-446655440000
# Create a resource
oneuptime monitor create --data '{"name":"API Health","projectId":"..."}'
# See all available resources
oneuptime resources
```
## Authentication & Contexts
The CLI supports multiple authentication contexts, making it easy to switch between environments.
### Setting Up
```bash
# Create a production context
oneuptime login sk-prod-key https://oneuptime.com --context-name production
# Create a staging context
oneuptime login sk-staging-key https://staging.oneuptime.com --context-name staging
```
### Switching Contexts
```bash
# List all contexts
oneuptime context list
# Switch active context
oneuptime context use staging
# Show current context
oneuptime context current
# Delete a context
oneuptime context delete old-context
```
### Credential Resolution Order
1. CLI flags: `--api-key` and `--url`
2. Environment variables: `ONEUPTIME_API_KEY` and `ONEUPTIME_URL`
3. Current context from config file (`~/.oneuptime/config.json`)
## Command Reference
### Authentication
| Command | Description |
|---|---|
| `oneuptime login <api-key> <url>` | Authenticate and create a context |
| `oneuptime context list` | List all contexts |
| `oneuptime context use <name>` | Switch active context |
| `oneuptime context current` | Show current context |
| `oneuptime context delete <name>` | Remove a context |
| `oneuptime whoami` | Show current auth info |
### Resource Operations
Every discovered resource supports these subcommands:
| Subcommand | Description |
|---|---|
| `<resource> list [options]` | List resources with filtering and pagination |
| `<resource> get <id>` | Get a single resource by ID |
| `<resource> create --data <json>` | Create a new resource |
| `<resource> update <id> --data <json>` | Update an existing resource |
| `<resource> delete <id>` | Delete a resource |
| `<resource> count [--query <json>]` | Count resources |
### List Options
```
--query <json> Filter criteria as JSON
--limit <n> Maximum number of results (default: 10)
--skip <n> Number of results to skip (default: 0)
--sort <json> Sort order as JSON (e.g. '{"createdAt": -1}')
-o, --output Output format: json, table, wide
```
### Utility Commands
| Command | Description |
|---|---|
| `oneuptime version` | Print CLI version |
| `oneuptime whoami` | Show current authentication info |
| `oneuptime resources` | List all available resource types |
## Output Formats
| Format | Description |
|---|---|
| `table` | Formatted ASCII table (default for TTY) |
| `json` | Raw JSON (default when piped) |
| `wide` | Table with all columns shown |
```bash
# Explicit format
oneuptime incident list -o json
oneuptime incident list -o table
oneuptime incident list -o wide
# Pipe to jq (auto-detects JSON)
oneuptime incident list | jq '.[].title'
```
## Scripting Examples
```bash
# List incidents as JSON for scripting
oneuptime incident list -o json --limit 100
# Count resources with filter
oneuptime incident count --query '{"currentIncidentStateId":"..."}'
# Create from a JSON file
oneuptime monitor create --file monitor.json
# Use environment variables in CI/CD
ONEUPTIME_API_KEY=sk-xxx ONEUPTIME_URL=https://oneuptime.com oneuptime incident list
```
## Environment Variables
| Variable | Description |
|---|---|
| `ONEUPTIME_API_KEY` | API key for authentication |
| `ONEUPTIME_URL` | OneUptime instance URL |
| `NO_COLOR` | Disable colored output |
## Configuration File
The CLI stores configuration at `~/.oneuptime/config.json` with `0600` permissions. The file contains:
```json
{
"currentContext": "production",
"contexts": {
"production": {
"name": "production",
"apiUrl": "https://oneuptime.com",
"apiKey": "sk-..."
}
},
"defaults": {
"output": "table",
"limit": 10
}
}
```
## Global Options
| Option | Description |
|---|---|
| `--api-key <key>` | Override API key for this command |
| `--url <url>` | Override instance URL for this command |
| `--context <name>` | Use a specific context for this command |
| `-o, --output <format>` | Output format: json, table, wide |
| `--no-color` | Disable colored output |
## Supported Resources
Run `oneuptime resources` to see all available resource types. Resources are auto-discovered from OneUptime models that have MCP enabled. Currently supported:
- **Incident** - Manage incidents
- **Alert** - Manage alerts
- **Monitor** - Manage monitors
- **Monitor Status** - Manage monitor statuses
- **Incident State** - Manage incident states
- **Status Page** - Manage status pages
- **On-Call Policy** - Manage on-call duty policies
- **Team** - Manage teams
- **Scheduled Maintenance Event** - Manage scheduled maintenance
As more models are MCP-enabled in OneUptime, they automatically become available in the CLI.
## Development
```bash
cd CLI
npm install
npm start -- --help # Run via ts-node
npm test # Run tests
npm run compile # Type-check
```
## License
Apache-2.0

386
CLI/Tests/ApiClient.test.ts Normal file
View File

@@ -0,0 +1,386 @@
import { executeApiRequest, ApiRequestOptions } from "../Core/ApiClient";
import API from "Common/Utils/API";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import { JSONValue } from "Common/Types/JSON";
// Mock the Common/Utils/API module
jest.mock("Common/Utils/API", () => {
const mockPost: jest.Mock = jest.fn();
const mockPut: jest.Mock = jest.fn();
const mockDelete: jest.Mock = jest.fn();
function MockAPI(
this: { protocol: string; hostname: string },
protocol: string,
hostname: string,
_route: string,
): void {
this.protocol = protocol;
this.hostname = hostname;
}
MockAPI.post = mockPost;
MockAPI.put = mockPut;
MockAPI.delete = mockDelete;
return {
__esModule: true,
default: MockAPI,
};
});
function createSuccessResponse(
data: Record<string, unknown> | Record<string, unknown>[],
): {
data: Record<string, unknown> | Record<string, unknown>[];
statusCode: number;
} {
return { data, statusCode: 200 };
}
function createErrorResponse(
statusCode: number,
message: string,
): HTTPErrorResponse {
/*
* HTTPErrorResponse computes `message` from `.data` via a getter.
* We create a proper prototype chain and set data to contain the message.
*/
const resp: HTTPErrorResponse = Object.create(HTTPErrorResponse.prototype);
resp.statusCode = statusCode;
/*
* HTTPResponse stores data in _jsonData and exposes it via `data` getter
* But since the prototype chain may not have full getters, we define them
*/
Object.defineProperty(resp, "data", {
get: (): { message: string } => {
return { message: message };
},
configurable: true,
});
return resp;
}
describe("ApiClient", () => {
let mockPost: jest.Mock;
let mockPut: jest.Mock;
let mockDelete: jest.Mock;
beforeEach(() => {
mockPost = API.post as jest.Mock;
mockPut = API.put as jest.Mock;
mockDelete = API.delete as jest.Mock;
(mockPost as jest.Mock).mockReset();
(mockPut as jest.Mock).mockReset();
(mockDelete as jest.Mock).mockReset();
});
const baseOptions: ApiRequestOptions = {
apiUrl: "https://oneuptime.com",
apiKey: "test-api-key",
apiPath: "/incident",
operation: "create",
};
describe("create operation", () => {
it("should make a POST request with data wrapped in { data: ... }", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ _id: "123" }),
);
const result: JSONValue = await executeApiRequest({
...baseOptions,
operation: "create",
data: { name: "Test Incident" },
});
expect(mockPost).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toEqual({ data: { name: "Test Incident" } });
expect(result).toEqual({ _id: "123" });
});
it("should use empty object when no data provided for create", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ _id: "123" }),
);
await executeApiRequest({
...baseOptions,
operation: "create",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toEqual({ data: {} });
});
});
describe("read operation", () => {
it("should make a POST request with select and id in route", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ _id: "abc", name: "Test" }),
);
const result: JSONValue = await executeApiRequest({
...baseOptions,
operation: "read",
id: "abc-123",
select: { _id: true, name: true },
});
expect(mockPost).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("abc-123/get-item");
expect(callArgs.data).toEqual({ select: { _id: true, name: true } });
expect(result).toEqual({ _id: "abc", name: "Test" });
});
it("should use empty select when none provided", async () => {
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "read",
id: "abc-123",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toEqual({ select: {} });
});
it("should build route without id when no id provided", async () => {
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "read",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("/api/incident");
expect(callArgs.url.toString()).not.toContain("/get-item");
});
});
describe("list operation", () => {
it("should make a POST request with query, select, skip, limit, sort", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ data: [] }),
);
await executeApiRequest({
...baseOptions,
operation: "list",
query: { status: "active" },
select: { _id: true },
skip: 5,
limit: 20,
sort: { createdAt: -1 },
});
expect(mockPost).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("/get-list");
expect(callArgs.data).toEqual({
query: { status: "active" },
select: { _id: true },
skip: 5,
limit: 20,
sort: { createdAt: -1 },
});
});
it("should use defaults when no query options provided", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ data: [] }),
);
await executeApiRequest({
...baseOptions,
operation: "list",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toEqual({
query: {},
select: {},
skip: 0,
limit: 10,
sort: {},
});
});
});
describe("count operation", () => {
it("should make a POST request to /count path", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createSuccessResponse({ count: 42 }),
);
const result: JSONValue = await executeApiRequest({
...baseOptions,
operation: "count",
query: { status: "active" },
});
expect(mockPost).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("/count");
expect(result).toEqual({ count: 42 });
});
});
describe("update operation", () => {
it("should make a PUT request with data", async () => {
(mockPut as jest.Mock).mockResolvedValue(
createSuccessResponse({ _id: "abc" }),
);
const result: JSONValue = await executeApiRequest({
...baseOptions,
operation: "update",
id: "abc-123",
data: { name: "Updated" },
});
expect(mockPut).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("abc-123");
expect(callArgs.data).toEqual({ data: { name: "Updated" } });
expect(result).toEqual({ _id: "abc" });
});
it("should use empty object when no data provided for update", async () => {
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "update",
id: "abc-123",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toEqual({ data: {} });
});
it("should build route without id when no id provided", async () => {
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "update",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("/api/incident");
});
});
describe("delete operation", () => {
it("should make a DELETE request", async () => {
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "delete",
id: "abc-123",
});
expect(mockDelete).toHaveBeenCalledTimes(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("abc-123");
expect(callArgs.data).toBeUndefined();
});
it("should build route without id when no id provided", async () => {
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "delete",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
expect(callArgs.url.toString()).toContain("/api/incident");
});
});
describe("error handling", () => {
it("should throw on HTTPErrorResponse", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createErrorResponse(500, "Server Error"),
);
await expect(
executeApiRequest({ ...baseOptions, operation: "create", data: {} }),
).rejects.toThrow("API error");
});
it("should include status code in error message", async () => {
(mockPost as jest.Mock).mockResolvedValue(
createErrorResponse(403, "Forbidden"),
);
await expect(
executeApiRequest({ ...baseOptions, operation: "list" }),
).rejects.toThrow("403");
});
it("should handle error response with no message", async () => {
(mockPost as jest.Mock).mockResolvedValue(createErrorResponse(500, ""));
await expect(
executeApiRequest({ ...baseOptions, operation: "list" }),
).rejects.toThrow("API error");
});
});
describe("headers", () => {
it("should include APIKey, Content-Type, and Accept headers", async () => {
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "create",
data: { name: "Test" },
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
expect(callArgs.headers["APIKey"]).toBe("test-api-key");
expect(callArgs.headers["Content-Type"]).toBe("application/json");
expect(callArgs.headers["Accept"]).toBe("application/json");
});
});
describe("default/unknown operation", () => {
it("should handle unknown operation in buildRequestData (falls to default)", async () => {
// The "delete" case hits the default branch in buildRequestData returning undefined
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
await executeApiRequest({
...baseOptions,
operation: "delete",
id: "123",
});
// Should not send data for delete
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
expect(callArgs.data).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,257 @@
import { Command } from "commander";
import { registerConfigCommands } from "../Commands/ConfigCommands";
import * as ConfigManager from "../Core/ConfigManager";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
describe("ConfigCommands", () => {
let originalConfigContent: string | null = null;
let consoleLogSpy: jest.SpyInstance;
let exitSpy: jest.SpyInstance;
beforeAll(() => {
if (fs.existsSync(CONFIG_FILE)) {
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
}
});
afterAll(() => {
if (originalConfigContent) {
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
} else if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
});
beforeEach(() => {
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
});
afterEach(() => {
consoleLogSpy.mockRestore();
jest.restoreAllMocks();
});
function createProgram(): Command {
const program: Command = new Command();
program.exitOverride(); // Prevent commander from calling process.exit
program.configureOutput({
writeOut: () => {},
writeErr: () => {},
});
registerConfigCommands(program);
return program;
}
describe("login command", () => {
it("should create a context and set it as current", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"login",
"my-api-key",
"https://example.com",
]);
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(ctx).not.toBeNull();
expect(ctx!.name).toBe("default");
expect(ctx!.apiUrl).toBe("https://example.com");
expect(ctx!.apiKey).toBe("my-api-key");
});
it("should use custom context name", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"login",
"key123",
"https://prod.com",
"--context-name",
"production",
]);
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(ctx!.name).toBe("production");
});
it("should handle login errors gracefully", async () => {
// Mock addContext to throw
const addCtxSpy: jest.SpyInstance = jest
.spyOn(ConfigManager, "addContext")
.mockImplementation(() => {
throw new Error("Permission denied");
});
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"login",
"key123",
"https://example.com",
]);
expect(exitSpy).toHaveBeenCalledWith(1);
addCtxSpy.mockRestore();
});
it("should strip trailing slashes from URL", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"login",
"key123",
"https://example.com///",
]);
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(ctx!.apiUrl).toBe("https://example.com");
});
});
describe("context list command", () => {
it("should show message when no contexts exist", async () => {
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "list"]);
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should list contexts with current marker", async () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "list"]);
expect(consoleLogSpy).toHaveBeenCalled();
});
});
describe("context use command", () => {
it("should switch to the specified context", async () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "use", "b"]);
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current!.name).toBe("b");
});
it("should handle non-existent context", async () => {
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "use", "nope"]);
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
describe("context current command", () => {
it("should show current context info", async () => {
ConfigManager.addContext({
name: "myctx",
apiUrl: "https://myctx.com",
apiKey: "abcdefghijklm",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "current"]);
// Check that masked key is shown
expect(consoleLogSpy).toHaveBeenCalledWith("Context: myctx");
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://myctx.com");
// Key should be masked: abcd****jklm
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("****"),
);
});
it("should show message when no current context", async () => {
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "current"]);
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should mask short API keys", async () => {
ConfigManager.addContext({
name: "short",
apiUrl: "https://s.com",
apiKey: "abc",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "context", "current"]);
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
});
});
describe("context delete command", () => {
it("should delete a context", async () => {
ConfigManager.addContext({
name: "todelete",
apiUrl: "https://del.com",
apiKey: "k1",
});
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"context",
"delete",
"todelete",
]);
const contexts: ReturnType<typeof ConfigManager.listContexts> =
ConfigManager.listContexts();
expect(contexts).toHaveLength(0);
});
it("should handle deletion of non-existent context", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"context",
"delete",
"nonexistent",
]);
expect(exitSpy).toHaveBeenCalledWith(1);
});
});
});

View File

@@ -0,0 +1,446 @@
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as ConfigManager from "../Core/ConfigManager";
import { CLIConfig, ResolvedCredentials } from "../Types/CLITypes";
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
describe("ConfigManager", () => {
let originalConfigContent: string | null = null;
beforeAll(() => {
if (fs.existsSync(CONFIG_FILE)) {
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
}
});
afterAll(() => {
if (originalConfigContent) {
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
} else if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
});
beforeEach(() => {
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
afterEach(() => {
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
describe("load", () => {
it("should return default config when no config file exists", () => {
const config: CLIConfig = ConfigManager.load();
expect(config.currentContext).toBe("");
expect(config.contexts).toEqual({});
expect(config.defaults.output).toBe("table");
expect(config.defaults.limit).toBe(10);
});
it("should load existing config from file", () => {
const testConfig: CLIConfig = {
currentContext: "test",
contexts: {
test: { name: "test", apiUrl: "https://test.com", apiKey: "key123" },
},
defaults: { output: "json", limit: 20 },
};
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(testConfig), {
mode: 0o600,
});
const config: CLIConfig = ConfigManager.load();
expect(config.currentContext).toBe("test");
expect(config.contexts["test"]?.apiKey).toBe("key123");
});
it("should return default config when file contains invalid JSON", () => {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, "not valid json {{{", { mode: 0o600 });
const config: CLIConfig = ConfigManager.load();
expect(config.currentContext).toBe("");
expect(config.contexts).toEqual({});
});
});
describe("save", () => {
it("should create config directory if it does not exist", () => {
// Remove the dir if it exists (we'll restore after)
const tmpDir: string = path.join(
os.tmpdir(),
".oneuptime-test-" + Date.now(),
);
/*
* We can't easily test this with the real path, but we verify save works
* when the dir already exists (which it does after beforeAll).
*/
const config: CLIConfig = {
currentContext: "",
contexts: {},
defaults: { output: "table", limit: 10 },
};
ConfigManager.save(config);
expect(fs.existsSync(CONFIG_FILE)).toBe(true);
void tmpDir; // unused but shows intent
});
it("should write config with correct permissions", () => {
const config: CLIConfig = {
currentContext: "x",
contexts: {
x: { name: "x", apiUrl: "https://x.com", apiKey: "k" },
},
defaults: { output: "table", limit: 10 },
};
ConfigManager.save(config);
const content: string = fs.readFileSync(CONFIG_FILE, "utf-8");
const parsed: CLIConfig = JSON.parse(content);
expect(parsed.currentContext).toBe("x");
});
});
describe("getCurrentContext", () => {
it("should return null when no current context is set", () => {
expect(ConfigManager.getCurrentContext()).toBeNull();
});
it("should return null when currentContext name does not match any context", () => {
// Manually write a config with a dangling currentContext reference
const config: CLIConfig = {
currentContext: "ghost",
contexts: {},
defaults: { output: "table", limit: 10 },
};
ConfigManager.save(config);
expect(ConfigManager.getCurrentContext()).toBeNull();
});
it("should return the current context when set", () => {
ConfigManager.addContext({
name: "prod",
apiUrl: "https://prod.com",
apiKey: "k1",
});
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(ctx).not.toBeNull();
expect(ctx!.name).toBe("prod");
});
});
describe("addContext", () => {
it("should add a context and set it as current if first context", () => {
ConfigManager.addContext({
name: "prod",
apiUrl: "https://prod.oneuptime.com",
apiKey: "sk-prod-123",
});
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current).not.toBeNull();
expect(current!.name).toBe("prod");
});
it("should not change current context when adding a second context", () => {
ConfigManager.addContext({
name: "prod",
apiUrl: "https://prod.com",
apiKey: "key1",
});
ConfigManager.addContext({
name: "staging",
apiUrl: "https://staging.com",
apiKey: "key2",
});
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current!.name).toBe("prod"); // First one remains current
});
it("should add multiple contexts", () => {
ConfigManager.addContext({
name: "prod",
apiUrl: "https://prod.com",
apiKey: "key1",
});
ConfigManager.addContext({
name: "staging",
apiUrl: "https://staging.com",
apiKey: "key2",
});
const contexts: ReturnType<typeof ConfigManager.listContexts> =
ConfigManager.listContexts();
expect(contexts).toHaveLength(2);
});
});
describe("setCurrentContext", () => {
it("should switch the active context", () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
ConfigManager.setCurrentContext("b");
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current!.name).toBe("b");
});
it("should throw for non-existent context", () => {
expect(() => {
return ConfigManager.setCurrentContext("nonexistent");
}).toThrow('Context "nonexistent" does not exist');
});
});
describe("removeContext", () => {
it("should remove a context", () => {
ConfigManager.addContext({
name: "test",
apiUrl: "https://test.com",
apiKey: "k1",
});
ConfigManager.removeContext("test");
const contexts: ReturnType<typeof ConfigManager.listContexts> =
ConfigManager.listContexts();
expect(contexts).toHaveLength(0);
});
it("should throw for non-existent context", () => {
expect(() => {
return ConfigManager.removeContext("nonexistent");
}).toThrow('Context "nonexistent" does not exist');
});
it("should update current context when removing the current one", () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
ConfigManager.setCurrentContext("a");
ConfigManager.removeContext("a");
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current).not.toBeNull();
expect(current!.name).toBe("b");
});
it("should set current context to empty when removing last context", () => {
ConfigManager.addContext({
name: "only",
apiUrl: "https://only.com",
apiKey: "k1",
});
ConfigManager.removeContext("only");
expect(ConfigManager.getCurrentContext()).toBeNull();
const config: CLIConfig = ConfigManager.load();
expect(config.currentContext).toBe("");
});
it("should not change current context when removing a non-current one", () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
ConfigManager.setCurrentContext("a");
ConfigManager.removeContext("b");
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
ConfigManager.getCurrentContext();
expect(current!.name).toBe("a");
});
});
describe("listContexts", () => {
it("should return empty array when no contexts exist", () => {
expect(ConfigManager.listContexts()).toEqual([]);
});
it("should mark current context correctly", () => {
ConfigManager.addContext({
name: "a",
apiUrl: "https://a.com",
apiKey: "k1",
});
ConfigManager.addContext({
name: "b",
apiUrl: "https://b.com",
apiKey: "k2",
});
ConfigManager.setCurrentContext("b");
const contexts: ReturnType<typeof ConfigManager.listContexts> =
ConfigManager.listContexts();
const a:
| ReturnType<typeof ConfigManager.listContexts>[number]
| undefined = contexts.find(
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
return c.name === "a";
},
);
const b:
| ReturnType<typeof ConfigManager.listContexts>[number]
| undefined = contexts.find(
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
return c.name === "b";
},
);
expect(a!.isCurrent).toBe(false);
expect(b!.isCurrent).toBe(true);
});
});
describe("getResolvedCredentials", () => {
it("should resolve from CLI options first", () => {
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
apiKey: "cli-key",
url: "https://cli.com",
});
expect(creds.apiKey).toBe("cli-key");
expect(creds.apiUrl).toBe("https://cli.com");
});
it("should resolve from env vars when CLI options are missing", () => {
process.env["ONEUPTIME_API_KEY"] = "env-key";
process.env["ONEUPTIME_URL"] = "https://env.com";
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
{},
);
expect(creds.apiKey).toBe("env-key");
expect(creds.apiUrl).toBe("https://env.com");
});
it("should resolve from --context flag", () => {
ConfigManager.addContext({
name: "named",
apiUrl: "https://named.com",
apiKey: "named-key",
});
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
context: "named",
});
expect(creds.apiKey).toBe("named-key");
expect(creds.apiUrl).toBe("https://named.com");
});
it("should throw when --context flag references non-existent context", () => {
expect(() => {
return ConfigManager.getResolvedCredentials({ context: "nope" });
}).toThrow('Context "nope" does not exist');
});
it("should resolve from current context in config", () => {
ConfigManager.addContext({
name: "ctx",
apiUrl: "https://ctx.com",
apiKey: "ctx-key",
});
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
{},
);
expect(creds.apiKey).toBe("ctx-key");
expect(creds.apiUrl).toBe("https://ctx.com");
});
it("should resolve from partial env vars (only ONEUPTIME_API_KEY)", () => {
process.env["ONEUPTIME_API_KEY"] = "partial-key";
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
{},
);
expect(creds.apiKey).toBe("partial-key");
expect(creds.apiUrl).toBe("");
});
it("should resolve from partial env vars (only ONEUPTIME_URL)", () => {
process.env["ONEUPTIME_URL"] = "https://partial.com";
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
{},
);
expect(creds.apiKey).toBe("");
expect(creds.apiUrl).toBe("https://partial.com");
});
it("should combine partial env var with context", () => {
process.env["ONEUPTIME_API_KEY"] = "env-key";
ConfigManager.addContext({
name: "ctx",
apiUrl: "https://ctx.com",
apiKey: "ctx-key",
});
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
{},
);
/*
* env vars take priority: both are set so goes through priority 2
* Actually, only ONEUPTIME_API_KEY is set, not ONEUPTIME_URL
* So it falls through to priority 4 (current context)
*/
expect(creds.apiKey).toBe("ctx-key");
expect(creds.apiUrl).toBe("https://ctx.com");
});
it("should throw when no credentials available at all", () => {
expect(() => {
return ConfigManager.getResolvedCredentials({});
}).toThrow("No credentials found");
});
it("should prefer CLI flags over env vars", () => {
process.env["ONEUPTIME_API_KEY"] = "env-key";
process.env["ONEUPTIME_URL"] = "https://env.com";
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials({
apiKey: "cli-key",
url: "https://cli.com",
});
expect(creds.apiKey).toBe("cli-key");
expect(creds.apiUrl).toBe("https://cli.com");
});
});
});

View File

@@ -0,0 +1,98 @@
import { handleError, ExitCode } from "../Core/ErrorHandler";
import * as OutputFormatter from "../Core/OutputFormatter";
describe("ErrorHandler", () => {
let exitSpy: jest.SpyInstance;
let printErrorSpy: jest.SpyInstance;
beforeEach(() => {
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {
// no-op
}) as any);
printErrorSpy = jest
.spyOn(OutputFormatter, "printError")
.mockImplementation(() => {
// no-op
});
});
afterEach(() => {
exitSpy.mockRestore();
printErrorSpy.mockRestore();
});
it("should exit with AuthError for API key errors", () => {
handleError(new Error("Invalid API key provided"));
expect(printErrorSpy).toHaveBeenCalledWith(
"Authentication error: Invalid API key provided",
);
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
});
it("should exit with AuthError for credentials errors", () => {
handleError(new Error("No credentials found"));
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
});
it("should exit with AuthError for Unauthorized errors", () => {
handleError(new Error("Unauthorized access"));
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
});
it("should exit with AuthError for 401 errors", () => {
handleError(new Error("HTTP 401 response"));
expect(exitSpy).toHaveBeenCalledWith(ExitCode.AuthError);
});
it("should exit with NotFound for 404 errors", () => {
handleError(new Error("HTTP 404 response"));
expect(printErrorSpy).toHaveBeenCalledWith("Not found: HTTP 404 response");
expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound);
});
it("should exit with NotFound for not found errors", () => {
handleError(new Error("Resource not found"));
expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound);
});
it("should exit with GeneralError for API error messages", () => {
handleError(new Error("API error (500): Internal Server Error"));
expect(printErrorSpy).toHaveBeenCalledWith(
"API error (500): Internal Server Error",
);
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
});
it("should exit with GeneralError for generic Error objects", () => {
handleError(new Error("Something went wrong"));
expect(printErrorSpy).toHaveBeenCalledWith("Error: Something went wrong");
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
});
it("should handle non-Error objects", () => {
handleError("string error");
expect(printErrorSpy).toHaveBeenCalledWith("Error: string error");
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
});
it("should handle null error", () => {
handleError(null);
expect(printErrorSpy).toHaveBeenCalledWith("Error: null");
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
});
it("should handle number error", () => {
handleError(42);
expect(printErrorSpy).toHaveBeenCalledWith("Error: 42");
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
});
describe("ExitCode enum", () => {
it("should have correct values", () => {
expect(ExitCode.Success).toBe(0);
expect(ExitCode.GeneralError).toBe(1);
expect(ExitCode.AuthError).toBe(2);
expect(ExitCode.NotFound).toBe(3);
});
});
});

66
CLI/Tests/Index.test.ts Normal file
View File

@@ -0,0 +1,66 @@
import { Command, Option } from "commander";
import { registerConfigCommands } from "../Commands/ConfigCommands";
import { registerResourceCommands } from "../Commands/ResourceCommands";
import { registerUtilityCommands } from "../Commands/UtilityCommands";
describe("Index (CLI entry point)", () => {
it("should create a program with all command groups registered", () => {
const program: Command = new Command();
program
.name("oneuptime")
.description(
"OneUptime CLI - Manage your OneUptime resources from the command line",
)
.version("1.0.0")
.option("--api-key <key>", "API key (overrides config)")
.option("--url <url>", "OneUptime instance URL (overrides config)")
.option("--context <name>", "Use a specific context")
.option("-o, --output <format>", "Output format: json, table, wide")
.option("--no-color", "Disable colored output");
registerConfigCommands(program);
registerUtilityCommands(program);
registerResourceCommands(program);
// Verify all expected commands are registered
const commandNames: string[] = program.commands.map((c: Command) => {
return c.name();
});
expect(commandNames).toContain("login");
expect(commandNames).toContain("context");
expect(commandNames).toContain("version");
expect(commandNames).toContain("whoami");
expect(commandNames).toContain("resources");
expect(commandNames).toContain("incident");
expect(commandNames).toContain("monitor");
expect(commandNames).toContain("alert");
});
it("should set correct program name and description", () => {
const program: Command = new Command();
program.name("oneuptime").description("OneUptime CLI");
expect(program.name()).toBe("oneuptime");
});
it("should define global options", () => {
const program: Command = new Command();
program
.option("--api-key <key>", "API key")
.option("--url <url>", "URL")
.option("--context <name>", "Context")
.option("-o, --output <format>", "Output format")
.option("--no-color", "Disable color");
// Parse with just the program name - verify options are registered
const options: readonly Option[] = program.options;
const optionNames: (string | undefined)[] = options.map((o: Option) => {
return o.long || o.short;
});
expect(optionNames).toContain("--api-key");
expect(optionNames).toContain("--url");
expect(optionNames).toContain("--context");
expect(optionNames).toContain("--output");
expect(optionNames).toContain("--no-color");
});
});

View File

@@ -0,0 +1,373 @@
import {
formatOutput,
printSuccess,
printError,
printWarning,
printInfo,
} from "../Core/OutputFormatter";
import { JSONObject } from "Common/Types/JSON";
describe("OutputFormatter", () => {
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: jest.SpyInstance;
let originalNoColor: string | undefined;
let originalArgv: string[];
beforeEach(() => {
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
originalNoColor = process.env["NO_COLOR"];
originalArgv = [...process.argv];
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
if (originalNoColor !== undefined) {
process.env["NO_COLOR"] = originalNoColor;
} else {
delete process.env["NO_COLOR"];
}
process.argv = originalArgv;
});
describe("formatOutput with JSON format", () => {
it("should format single object as JSON", () => {
const data: Record<string, string> = { id: "123", name: "Test" };
const result: string = formatOutput(data, "json");
expect(JSON.parse(result)).toEqual(data);
});
it("should format array as JSON", () => {
const data: Record<string, string>[] = [
{ id: "1", name: "A" },
{ id: "2", name: "B" },
];
const result: string = formatOutput(data, "json");
expect(JSON.parse(result)).toEqual(data);
});
it("should format null as JSON", () => {
const result: string = formatOutput(null, "json");
expect(result).toBe("null");
});
it("should format number as JSON", () => {
const result: string = formatOutput(42, "json");
expect(result).toBe("42");
});
it("should format string as JSON", () => {
const result: string = formatOutput("hello", "json");
expect(result).toBe('"hello"');
});
it("should format boolean as JSON", () => {
const result: string = formatOutput(true, "json");
expect(result).toBe("true");
});
});
describe("formatOutput with table format", () => {
it("should format array as table", () => {
const data: Record<string, string>[] = [
{ _id: "1", name: "A" },
{ _id: "2", name: "B" },
];
const result: string = formatOutput(data, "table");
expect(result).toContain("1");
expect(result).toContain("A");
expect(result).toContain("2");
expect(result).toContain("B");
});
it("should handle empty array", () => {
const result: string = formatOutput([], "table");
expect(result).toBe("No results found.");
});
it("should handle single object as key-value table", () => {
const data: Record<string, string> = { name: "Test", status: "Active" };
const result: string = formatOutput(data, "table");
expect(result).toContain("Test");
expect(result).toContain("Active");
});
it("should return 'No data returned.' for null in table mode", () => {
const result: string = formatOutput(null, "table");
expect(result).toBe("No data returned.");
});
it("should return 'No data returned.' for undefined in table mode", () => {
const result: string = formatOutput(undefined as any, "table");
expect(result).toBe("No data returned.");
});
it("should return 'No data returned.' for empty string in table mode", () => {
const result: string = formatOutput("" as any, "table");
expect(result).toBe("No data returned.");
});
it("should fallback to JSON for array of non-objects", () => {
const data: string[] = ["a", "b", "c"];
const result: string = formatOutput(data, "table");
// First item is not an object, so should fallback to JSON
expect(result).toContain('"a"');
});
it("should truncate long string values", () => {
const longValue: string = "x".repeat(100);
const data: Record<string, string>[] = [{ _id: "1", field: longValue }];
const result: string = formatOutput(data, "table");
expect(result).toContain("...");
});
it("should truncate long object values", () => {
const bigObj: Record<string, string> = { a: "x".repeat(80) };
const data: JSONObject[] = [{ _id: "1", nested: bigObj }];
const result: string = formatOutput(data, "table");
expect(result).toContain("...");
});
it("should show short object values without truncation", () => {
const smallObj: Record<string, number> = { a: 1 };
const data: JSONObject[] = [{ _id: "1", nested: smallObj }];
const result: string = formatOutput(data, "table");
expect(result).toContain('{"a":1}');
});
it("should render null values as empty in table", () => {
const data: JSONObject[] = [{ _id: "1", value: null }];
const result: string = formatOutput(data, "table");
expect(result).toContain("1");
});
it("should render undefined values as empty in table", () => {
const data: JSONObject[] = [{ _id: "1", value: undefined }];
const result: string = formatOutput(data, "table");
expect(result).toContain("1");
});
});
describe("formatOutput with wide format", () => {
it("should show all columns in wide mode", () => {
const data: Record<string, string>[] = [
{
_id: "1",
name: "A",
col1: "x",
col2: "y",
col3: "z",
col4: "w",
col5: "v",
col6: "u",
col7: "t",
},
];
const result: string = formatOutput(data, "wide");
expect(result).toContain("col7");
});
it("should limit columns in non-wide table mode", () => {
const data: Record<string, string>[] = [
{
_id: "1",
name: "A",
col1: "x",
col2: "y",
col3: "z",
col4: "w",
col5: "v",
col6: "u",
col7: "t",
},
];
const result: string = formatOutput(data, "table");
// Table mode should limit to 6 columns, so col7 should not appear
expect(result).not.toContain("col7");
});
it("should prioritize common columns in non-wide mode", () => {
const data: Record<string, string>[] = [
{
extra1: "a",
extra2: "b",
extra3: "c",
extra4: "d",
extra5: "e",
extra6: "f",
_id: "1",
name: "Test",
title: "Title",
status: "Active",
createdAt: "2024-01-01",
updatedAt: "2024-01-02",
},
];
const result: string = formatOutput(data, "table");
// Priority columns should appear
expect(result).toContain("_id");
expect(result).toContain("name");
});
});
describe("format auto-detection", () => {
it("should default to JSON when not a TTY", () => {
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", {
value: false,
writable: true,
configurable: true,
});
const data: Record<string, string> = { id: "1" };
const result: string = formatOutput(data);
expect(() => {
return JSON.parse(result);
}).not.toThrow();
Object.defineProperty(process.stdout, "isTTY", {
value: originalIsTTY,
writable: true,
configurable: true,
});
});
it("should default to table when TTY", () => {
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", {
value: true,
writable: true,
configurable: true,
});
const data: Record<string, string>[] = [{ _id: "1", name: "Test" }];
const result: string = formatOutput(data);
// Table format contains box-drawing characters
expect(result).toContain("\u2500");
Object.defineProperty(process.stdout, "isTTY", {
value: originalIsTTY,
writable: true,
configurable: true,
});
});
it("should handle unknown format string and default to table via TTY check", () => {
const data: Record<string, string>[] = [{ _id: "1" }];
// "unknown" is not json/table/wide, so cliFormat falls through and TTY detection occurs
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", {
value: true,
writable: true,
configurable: true,
});
const result: string = formatOutput(data, "unknown");
expect(result).toContain("\u2500");
Object.defineProperty(process.stdout, "isTTY", {
value: originalIsTTY,
writable: true,
configurable: true,
});
});
});
describe("color handling", () => {
it("should respect NO_COLOR env variable in table rendering", () => {
process.env["NO_COLOR"] = "1";
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
const result: string = formatOutput(data, "table");
// Should not contain ANSI color codes
// eslint-disable-next-line no-control-regex
expect(result).not.toMatch(/\x1b\[/);
});
it("should respect --no-color argv flag in table rendering", () => {
process.argv.push("--no-color");
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
const result: string = formatOutput(data, "table");
// eslint-disable-next-line no-control-regex
expect(result).not.toMatch(/\x1b\[/);
});
it("should render single object without color when NO_COLOR set", () => {
process.env["NO_COLOR"] = "1";
const data: Record<string, string> = { name: "Test" };
const result: string = formatOutput(data, "table");
// eslint-disable-next-line no-control-regex
expect(result).not.toMatch(/\x1b\[/);
expect(result).toContain("name");
});
});
describe("printSuccess", () => {
it("should log success message with color", () => {
delete process.env["NO_COLOR"];
// Remove --no-color from argv if present
process.argv = process.argv.filter((a: string) => {
return a !== "--no-color";
});
printSuccess("OK");
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should log success message without color when NO_COLOR is set", () => {
process.env["NO_COLOR"] = "1";
printSuccess("OK");
expect(consoleLogSpy).toHaveBeenCalledWith("OK");
});
});
describe("printError", () => {
it("should log error message with color", () => {
delete process.env["NO_COLOR"];
process.argv = process.argv.filter((a: string) => {
return a !== "--no-color";
});
printError("fail");
expect(consoleErrorSpy).toHaveBeenCalled();
});
it("should log error message without color when NO_COLOR is set", () => {
process.env["NO_COLOR"] = "1";
printError("fail");
expect(consoleErrorSpy).toHaveBeenCalledWith("fail");
});
});
describe("printWarning", () => {
it("should log warning message with color", () => {
delete process.env["NO_COLOR"];
process.argv = process.argv.filter((a: string) => {
return a !== "--no-color";
});
printWarning("warn");
expect(consoleErrorSpy).toHaveBeenCalled();
});
it("should log warning message without color when NO_COLOR is set", () => {
process.env["NO_COLOR"] = "1";
printWarning("warn");
expect(consoleErrorSpy).toHaveBeenCalledWith("warn");
});
});
describe("printInfo", () => {
it("should log info message with color", () => {
delete process.env["NO_COLOR"];
process.argv = process.argv.filter((a: string) => {
return a !== "--no-color";
});
printInfo("info");
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should log info message without color when NO_COLOR is set", () => {
process.env["NO_COLOR"] = "1";
printInfo("info");
expect(consoleLogSpy).toHaveBeenCalledWith("info");
});
});
});

View File

@@ -0,0 +1,568 @@
import { Command } from "commander";
import { ResourceInfo } from "../Types/CLITypes";
import * as ConfigManager from "../Core/ConfigManager";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
// Mock the ApiClient module before it's imported by ResourceCommands
const mockExecuteApiRequest: jest.Mock = jest.fn();
jest.mock("../Core/ApiClient", () => {
return {
...jest.requireActual("../Core/ApiClient"),
executeApiRequest: (...args: unknown[]): unknown => {
return mockExecuteApiRequest(...args);
},
};
});
// Import after mock setup
import {
discoverResources,
registerResourceCommands,
} from "../Commands/ResourceCommands";
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
describe("ResourceCommands", () => {
let originalConfigContent: string | null = null;
beforeAll(() => {
if (fs.existsSync(CONFIG_FILE)) {
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
}
});
afterAll(() => {
if (originalConfigContent) {
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
} else if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
});
beforeEach(() => {
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
mockExecuteApiRequest.mockReset();
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
afterEach(() => {
jest.restoreAllMocks();
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
describe("discoverResources", () => {
let resources: ResourceInfo[];
beforeAll(() => {
resources = discoverResources();
});
it("should discover at least one resource", () => {
expect(resources.length).toBeGreaterThan(0);
});
it("should discover the Incident resource", () => {
const incident: ResourceInfo | undefined = resources.find(
(r: ResourceInfo) => {
return r.singularName === "Incident";
},
);
expect(incident).toBeDefined();
expect(incident!.modelType).toBe("database");
expect(incident!.apiPath).toBe("/incident");
});
it("should discover the Monitor resource", () => {
const monitor: ResourceInfo | undefined = resources.find(
(r: ResourceInfo) => {
return r.singularName === "Monitor";
},
);
expect(monitor).toBeDefined();
expect(monitor!.modelType).toBe("database");
});
it("should discover the Alert resource", () => {
const alert: ResourceInfo | undefined = resources.find(
(r: ResourceInfo) => {
return r.singularName === "Alert";
},
);
expect(alert).toBeDefined();
});
it("should have kebab-case names for all resources", () => {
for (const r of resources) {
expect(r.name).toMatch(/^[a-z][a-z0-9-]*$/);
}
});
it("should have apiPath for all resources", () => {
for (const r of resources) {
expect(r.apiPath).toBeTruthy();
expect(r.apiPath.startsWith("/")).toBe(true);
}
});
it("should have valid modelType for all resources", () => {
for (const r of resources) {
expect(["database", "analytics"]).toContain(r.modelType);
}
});
});
describe("registerResourceCommands", () => {
it("should register commands for all discovered resources", () => {
const program: Command = new Command();
program.exitOverride();
registerResourceCommands(program);
const resources: ResourceInfo[] = discoverResources();
for (const resource of resources) {
const cmd: Command | undefined = program.commands.find((c: Command) => {
return c.name() === resource.name;
});
expect(cmd).toBeDefined();
}
});
it("should register list, get, create, update, delete, count subcommands for database resources", () => {
const program: Command = new Command();
program.exitOverride();
registerResourceCommands(program);
const incidentCmd: Command | undefined = program.commands.find(
(c: Command) => {
return c.name() === "incident";
},
);
expect(incidentCmd).toBeDefined();
const subcommands: string[] = incidentCmd!.commands.map((c: Command) => {
return c.name();
});
expect(subcommands).toContain("list");
expect(subcommands).toContain("get");
expect(subcommands).toContain("create");
expect(subcommands).toContain("update");
expect(subcommands).toContain("delete");
expect(subcommands).toContain("count");
});
});
describe("resource command actions", () => {
function createProgramWithResources(): Command {
const program: Command = new Command();
program.exitOverride();
program.configureOutput({
writeOut: () => {},
writeErr: () => {},
});
program
.option("--api-key <key>", "API key")
.option("--url <url>", "URL")
.option("--context <name>", "Context");
registerResourceCommands(program);
return program;
}
beforeEach(() => {
ConfigManager.addContext({
name: "test",
apiUrl: "https://test.oneuptime.com",
apiKey: "test-key-12345",
});
mockExecuteApiRequest.mockResolvedValue({ data: [] });
});
describe("list subcommand", () => {
it("should call API with list operation", async () => {
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "list"]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("list");
expect(mockExecuteApiRequest.mock.calls[0][0].apiPath).toBe(
"/incident",
);
});
it("should pass query, limit, skip, sort options", async () => {
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"list",
"--query",
'{"status":"active"}',
"--limit",
"20",
"--skip",
"5",
"--sort",
'{"createdAt":-1}',
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
const opts: Record<string, unknown> =
mockExecuteApiRequest.mock.calls[0][0];
expect(opts.query).toEqual({ status: "active" });
expect(opts.limit).toBe(20);
expect(opts.skip).toBe(5);
expect(opts.sort).toEqual({ createdAt: -1 });
});
it("should extract data array from response object", async () => {
mockExecuteApiRequest.mockResolvedValue({
data: [{ _id: "1", name: "Test" }],
});
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"list",
"-o",
"json",
]);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalled();
});
it("should handle response that is already an array", async () => {
mockExecuteApiRequest.mockResolvedValue([{ _id: "1" }]);
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"list",
"-o",
"json",
]);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalled();
});
it("should handle API errors", async () => {
mockExecuteApiRequest.mockRejectedValue(
new Error("API error (500): Server Error"),
);
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "list"]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("get subcommand", () => {
it("should call API with read operation and id", async () => {
mockExecuteApiRequest.mockResolvedValue({
_id: "abc-123",
name: "Test",
});
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"get",
"abc-123",
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
const opts: Record<string, unknown> =
mockExecuteApiRequest.mock.calls[0][0];
expect(opts.operation).toBe("read");
expect(opts.id).toBe("abc-123");
});
it("should support output format flag", async () => {
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"get",
"abc-123",
"-o",
"json",
]);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalled();
});
it("should handle get errors", async () => {
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"get",
"abc-123",
]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("create subcommand", () => {
it("should call API with create operation and data", async () => {
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"create",
"--data",
'{"name":"New Incident"}',
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
const opts: Record<string, unknown> =
mockExecuteApiRequest.mock.calls[0][0];
expect(opts.operation).toBe("create");
expect(opts.data).toEqual({ name: "New Incident" });
});
it("should support reading data from a file", async () => {
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
const tmpFile: string = path.join(
os.tmpdir(),
"cli-test-" + Date.now() + ".json",
);
fs.writeFileSync(tmpFile, '{"name":"From File"}');
try {
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"create",
"--file",
tmpFile,
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteApiRequest.mock.calls[0][0].data).toEqual({
name: "From File",
});
} finally {
fs.unlinkSync(tmpFile);
}
});
it("should error when neither --data nor --file is provided", async () => {
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "create"]);
expect(process.exit).toHaveBeenCalled();
});
it("should error on invalid JSON in --data", async () => {
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"create",
"--data",
"not-json",
]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("update subcommand", () => {
it("should call API with update operation, id, and data", async () => {
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"update",
"abc-123",
"--data",
'{"name":"Updated"}',
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
const opts: Record<string, unknown> =
mockExecuteApiRequest.mock.calls[0][0];
expect(opts.operation).toBe("update");
expect(opts.id).toBe("abc-123");
expect(opts.data).toEqual({ name: "Updated" });
});
it("should handle update errors", async () => {
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"update",
"abc-123",
"--data",
'{"name":"x"}',
]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("delete subcommand", () => {
it("should call API with delete operation and id", async () => {
mockExecuteApiRequest.mockResolvedValue({});
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"delete",
"abc-123",
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
const opts: Record<string, unknown> =
mockExecuteApiRequest.mock.calls[0][0];
expect(opts.operation).toBe("delete");
expect(opts.id).toBe("abc-123");
});
it("should handle API errors", async () => {
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"delete",
"abc-123",
]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("count subcommand", () => {
it("should call API with count operation", async () => {
mockExecuteApiRequest.mockResolvedValue({ count: 42 });
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "count"]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("count");
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(42);
});
it("should pass query filter", async () => {
mockExecuteApiRequest.mockResolvedValue({ count: 5 });
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"incident",
"count",
"--query",
'{"status":"active"}',
]);
expect(mockExecuteApiRequest.mock.calls[0][0].query).toEqual({
status: "active",
});
});
it("should handle response without count field", async () => {
mockExecuteApiRequest.mockResolvedValue(99);
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "count"]);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith(99);
});
it("should handle non-object response in count", async () => {
mockExecuteApiRequest.mockResolvedValue("some-string");
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "count"]);
// eslint-disable-next-line no-console
expect(console.log).toHaveBeenCalledWith("some-string");
});
it("should handle count errors", async () => {
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
const program: Command = createProgramWithResources();
await program.parseAsync(["node", "test", "incident", "count"]);
expect(process.exit).toHaveBeenCalled();
});
});
describe("credential resolution in commands", () => {
it("should use global --api-key and --url flags", async () => {
ConfigManager.removeContext("test");
mockExecuteApiRequest.mockResolvedValue({ data: [] });
const program: Command = createProgramWithResources();
await program.parseAsync([
"node",
"test",
"--api-key",
"global-key",
"--url",
"https://global.com",
"incident",
"list",
]);
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
expect(mockExecuteApiRequest.mock.calls[0][0].apiKey).toBe(
"global-key",
);
expect(mockExecuteApiRequest.mock.calls[0][0].apiUrl).toBe(
"https://global.com",
);
});
});
});
});

View File

@@ -0,0 +1,208 @@
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
import { JSONObject } from "Common/Types/JSON";
describe("SelectFieldGenerator", () => {
describe("generateAllFieldsSelect", () => {
describe("database models", () => {
it("should return fields for a known database model (Incident)", () => {
const select: JSONObject = generateAllFieldsSelect(
"Incident",
"database",
);
expect(Object.keys(select).length).toBeGreaterThan(0);
// Should have some common fields
expect(select).toHaveProperty("_id");
});
it("should return fields for Monitor model", () => {
const select: JSONObject = generateAllFieldsSelect(
"Monitor",
"database",
);
expect(Object.keys(select).length).toBeGreaterThan(0);
});
it("should return default select for unknown database model", () => {
const select: JSONObject = generateAllFieldsSelect(
"NonExistentModel12345",
"database",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
});
it("should filter fields based on access control", () => {
// Testing with a real model that has access control
const select: JSONObject = generateAllFieldsSelect(
"Incident",
"database",
);
// We just verify it returns something reasonable
expect(typeof select).toBe("object");
expect(Object.keys(select).length).toBeGreaterThan(0);
});
});
describe("analytics models", () => {
it("should return default select for known analytics model (LogItem)", () => {
// The Log analytics model has tableName "LogItem"
const select: JSONObject = generateAllFieldsSelect(
"LogItem",
"analytics",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
});
it("should return default select for unknown analytics model", () => {
const select: JSONObject = generateAllFieldsSelect(
"NonExistentAnalytics",
"analytics",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
});
});
describe("edge cases", () => {
it("should return default select for unknown model type", () => {
const select: JSONObject = generateAllFieldsSelect(
"Incident",
"unknown" as any,
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
});
it("should return default select for empty tableName", () => {
const select: JSONObject = generateAllFieldsSelect("", "database");
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
});
it("should handle outer exception and return default select", () => {
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
const DatabaseModels: Record<string, unknown> =
require("Common/Models/DatabaseModels/Index").default;
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
const origFind: unknown = DatabaseModels.find;
try {
DatabaseModels.find = (): never => {
throw new Error("Simulated error");
};
const select: JSONObject = generateAllFieldsSelect(
"Incident",
"database",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
} finally {
DatabaseModels.find = origFind;
}
});
it("should return default when getTableColumns returns empty", () => {
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
const tableColumnModule: Record<
string,
unknown
> = require("Common/Types/Database/TableColumn");
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
try {
tableColumnModule.getTableColumns = (): Record<string, unknown> => {
return {};
};
const select: JSONObject = generateAllFieldsSelect(
"Incident",
"database",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
} finally {
tableColumnModule.getTableColumns = origGetTableColumns;
}
});
it("should return default when all columns are filtered out", () => {
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
const tableColumnModule: Record<
string,
unknown
> = require("Common/Types/Database/TableColumn");
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
const DatabaseModels: Record<string, unknown> =
require("Common/Models/DatabaseModels/Index").default;
const origFind: unknown = DatabaseModels.find;
const Permission: Record<string, unknown> =
require("Common/Types/Permission").default;
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
try {
tableColumnModule.getTableColumns = (): Record<
string,
Record<string, unknown>
> => {
return { field1: {}, field2: {} };
};
DatabaseModels.find = (fn: (model: unknown) => boolean): unknown => {
function MockModel(this: Record<string, unknown>): void {
this.tableName = "MockTable";
this.getColumnAccessControlForAllColumns = (): Record<
string,
unknown
> => {
return {
field1: { read: [Permission.CurrentUser] },
field2: { read: [Permission.CurrentUser] },
};
};
}
const matches: boolean = fn(MockModel);
if (matches) {
return MockModel;
}
return undefined;
};
const select: JSONObject = generateAllFieldsSelect(
"MockTable",
"database",
);
expect(select).toEqual({
_id: true,
createdAt: true,
updatedAt: true,
});
} finally {
DatabaseModels.find = origFind;
tableColumnModule.getTableColumns = origGetTableColumns;
}
});
});
});
});

View File

@@ -0,0 +1,194 @@
import { Command } from "commander";
import { registerUtilityCommands } from "../Commands/UtilityCommands";
import * as ConfigManager from "../Core/ConfigManager";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
describe("UtilityCommands", () => {
let originalConfigContent: string | null = null;
let consoleLogSpy: jest.SpyInstance;
let exitSpy: jest.SpyInstance;
beforeAll(() => {
if (fs.existsSync(CONFIG_FILE)) {
originalConfigContent = fs.readFileSync(CONFIG_FILE, "utf-8");
}
});
afterAll(() => {
if (originalConfigContent) {
fs.writeFileSync(CONFIG_FILE, originalConfigContent, { mode: 0o600 });
} else if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
});
beforeEach(() => {
if (fs.existsSync(CONFIG_FILE)) {
fs.unlinkSync(CONFIG_FILE);
}
consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {}) as any);
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
afterEach(() => {
jest.restoreAllMocks();
delete process.env["ONEUPTIME_API_KEY"];
delete process.env["ONEUPTIME_URL"];
});
function createProgram(): Command {
const program: Command = new Command();
program.exitOverride();
program.configureOutput({
writeOut: () => {},
writeErr: () => {},
});
program
.option("--api-key <key>", "API key")
.option("--url <url>", "URL")
.option("--context <name>", "Context");
registerUtilityCommands(program);
return program;
}
describe("version command", () => {
it("should print version", async () => {
const program: Command = createProgram();
await program.parseAsync(["node", "test", "version"]);
expect(consoleLogSpy).toHaveBeenCalled();
// Should print a version string (either from package.json or fallback)
const versionArg: string = consoleLogSpy.mock.calls[0][0];
expect(typeof versionArg).toBe("string");
});
});
describe("whoami command", () => {
it("should show not authenticated when no credentials", async () => {
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should show credentials from current context", async () => {
ConfigManager.addContext({
name: "test",
apiUrl: "https://test.com",
apiKey: "abcdefghijklm",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://test.com");
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining("****"),
);
expect(consoleLogSpy).toHaveBeenCalledWith("Context: test");
});
it("should mask short API keys", async () => {
ConfigManager.addContext({
name: "short",
apiUrl: "https://s.com",
apiKey: "abc",
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
});
it("should show credentials from env vars", async () => {
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
process.env["ONEUPTIME_URL"] = "https://env.com";
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://env.com");
});
it("should handle whoami outer catch block", async () => {
// Mock getCurrentContext to throw an unexpected error
const spy: jest.SpyInstance = jest
.spyOn(ConfigManager, "getCurrentContext")
.mockImplementation(() => {
throw new Error("Unexpected crash");
});
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
expect(exitSpy).toHaveBeenCalledWith(1);
spy.mockRestore();
});
it("should not show context line when no context exists", async () => {
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
process.env["ONEUPTIME_URL"] = "https://env.com";
const program: Command = createProgram();
await program.parseAsync(["node", "test", "whoami"]);
// Should NOT have a "Context:" call since no context is set
const contextCalls: any[][] = consoleLogSpy.mock.calls.filter(
(call: any[]) => {
return typeof call[0] === "string" && call[0].startsWith("Context:");
},
);
expect(contextCalls).toHaveLength(0);
});
});
describe("resources command", () => {
it("should list all resources", async () => {
/*
* We need registerResourceCommands for discoverResources to work
* but discoverResources is imported directly, so it should work
*/
const program: Command = createProgram();
await program.parseAsync(["node", "test", "resources"]);
expect(consoleLogSpy).toHaveBeenCalled();
// Should show total count
const lastCall: string =
consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0];
expect(lastCall).toContain("Total:");
});
it("should filter by type", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"resources",
"--type",
"database",
]);
expect(consoleLogSpy).toHaveBeenCalled();
});
it("should show message when filter returns no results", async () => {
const program: Command = createProgram();
await program.parseAsync([
"node",
"test",
"resources",
"--type",
"nonexistent",
]);
expect(consoleLogSpy).toHaveBeenCalled();
});
});
});

34
CLI/Types/CLITypes.ts Normal file
View File

@@ -0,0 +1,34 @@
export interface CLIContext {
name: string;
apiUrl: string;
apiKey: string;
}
export interface CLIConfig {
currentContext: string;
contexts: Record<string, CLIContext>;
defaults: {
output: string;
limit: number;
};
}
export enum OutputFormat {
JSON = "json",
Table = "table",
Wide = "wide",
}
export interface ResourceInfo {
name: string;
singularName: string;
pluralName: string;
apiPath: string;
tableName: string;
modelType: "database" | "analytics";
}
export interface ResolvedCredentials {
apiUrl: string;
apiKey: string;
}

View File

@@ -0,0 +1,116 @@
import DatabaseModels from "Common/Models/DatabaseModels/Index";
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { getTableColumns } from "Common/Types/Database/TableColumn";
import Permission from "Common/Types/Permission";
import { JSONObject } from "Common/Types/JSON";
interface ColumnAccessControl {
read?: Permission[];
}
function shouldIncludeField(
columnName: string,
accessControlForColumns: Record<string, ColumnAccessControl>,
): boolean {
const accessControl: ColumnAccessControl | undefined =
accessControlForColumns[columnName];
return (
!accessControl ||
(accessControl.read !== undefined &&
accessControl.read.length > 0 &&
!(
accessControl.read.length === 1 &&
accessControl.read[0] === Permission.CurrentUser
))
);
}
export function generateAllFieldsSelect(
tableName: string,
modelType: "database" | "analytics",
): JSONObject {
try {
if (modelType === "database") {
const ModelClass: (new () => BaseModel) | undefined = DatabaseModels.find(
(Model: new () => BaseModel): boolean => {
try {
const instance: BaseModel = new Model();
return instance.tableName === tableName;
} catch {
return false;
}
},
);
if (!ModelClass) {
return getDefaultSelect();
}
const modelInstance: BaseModel = new ModelClass();
const tableColumns: Record<string, unknown> =
getTableColumns(modelInstance);
const columnNames: string[] = Object.keys(tableColumns);
if (columnNames.length === 0) {
return getDefaultSelect();
}
const accessControlForColumns: Record<string, ColumnAccessControl> =
(
modelInstance as unknown as {
getColumnAccessControlForAllColumns?: () => Record<
string,
ColumnAccessControl
>;
}
).getColumnAccessControlForAllColumns?.() || {};
const selectObject: JSONObject = {};
for (const columnName of columnNames) {
if (shouldIncludeField(columnName, accessControlForColumns)) {
selectObject[columnName] = true;
}
}
if (Object.keys(selectObject).length === 0) {
return getDefaultSelect();
}
return selectObject;
}
if (modelType === "analytics") {
const ModelClass: (new () => AnalyticsBaseModel) | undefined =
AnalyticsModels.find((Model: new () => AnalyticsBaseModel): boolean => {
try {
const instance: AnalyticsBaseModel = new Model();
return instance.tableName === tableName;
} catch {
return false;
}
});
if (!ModelClass) {
return getDefaultSelect();
}
// For analytics models, just return a basic select
return getDefaultSelect();
}
return getDefaultSelect();
} catch {
return getDefaultSelect();
}
}
function getDefaultSelect(): JSONObject {
return {
_id: true,
createdAt: true,
updatedAt: true,
};
}

35
CLI/jest.config.json Normal file
View File

@@ -0,0 +1,35 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
"collectCoverageFrom": [
"**/*.ts",
"!**/*.d.ts",
"!**/node_modules/**",
"!**/build/**",
"!**/Tests/**",
"!Index.ts"
],
"setupFilesAfterEnv": [],
"testTimeout": 30000,
"modulePathIgnorePatterns": ["<rootDir>/build/"],
"moduleNameMapper": {
"^Common/(.*)$": "<rootDir>/../Common/$1"
},
"transformIgnorePatterns": [
"node_modules/(?!(@oneuptime)/)"
],
"transform": {
"^.+\\.ts$": ["ts-jest", {
"tsconfig": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"strict": false,
"noImplicitAny": false,
"noImplicitThis": false,
"noPropertyAccessFromIndexSignature": false,
"module": "commonjs"
}
}]
}
}

17176
CLI/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
CLI/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "@oneuptime/cli",
"version": "1.0.0",
"description": "OneUptime CLI - Command-line interface for managing OneUptime resources",
"repository": {
"type": "git",
"url": "https://github.com/OneUptime/oneuptime"
},
"main": "Index.ts",
"bin": {
"oneuptime": "./Index.ts"
},
"scripts": {
"start": "node --require ts-node/register Index.ts",
"build": "npm run compile",
"compile": "tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"dev": "npx nodemon",
"audit": "npm audit --audit-level=low",
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
"test": "jest --passWithNoTests",
"link": "npm link"
},
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
"license": "Apache-2.0",
"dependencies": {
"Common": "npm:@oneuptime/common@latest",
"commander": "^12.1.0",
"chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"ts-node": "^10.9.2"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.15.21",
"jest": "^29.7.0",
"nodemon": "^3.1.11",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
}
}

44
CLI/tsconfig.json Normal file
View File

@@ -0,0 +1,44 @@
{
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"resolveJsonModule": true
}
},
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"jsx": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types"
],
"types": ["node", "jest"],
"sourceMap": true,
"outDir": "./build/dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"exclude": ["Tests", "build", "node_modules", "jest.config.json"]
}

View File

@@ -58,6 +58,25 @@ export default class GlobalConfig extends GlobalConfigModel {
})
public disableSignup?: boolean = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.Boolean,
title: "Disable User Project Creation",
description: "Only master admins can create projects when enabled.",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
nullable: true,
default: false,
unique: true,
})
public disableUserProjectCreation?: boolean = undefined;
// SMTP Settings.
@ColumnAccessControl({

View File

@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
import UserCallService, {
Service as UserCallServiceType,
} from "../Services/UserCallService";
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
import {
ExpressRequest,
ExpressResponse,
@@ -9,8 +10,10 @@ import {
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import logger from "../Utils/Logger";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import UserCall from "../../Models/DatabaseModels/UserCall";
import UserSMS from "../../Models/DatabaseModels/UserSMS";
@@ -52,6 +55,7 @@ export default class UserCallAPI extends BaseAPI<
},
select: {
userId: true,
projectId: true,
verificationCode: true,
},
});
@@ -95,6 +99,21 @@ export default class UserCallAPI extends BaseAPI<
},
});
// Create default notification rules for this verified call number
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(item.projectId!.toString()),
userId: new ObjectID(item.userId!.toString()),
notificationMethod: {
userCallId: item.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);

View File

@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
import UserEmailService, {
Service as UserEmailServiceType,
} from "../Services/UserEmailService";
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
import {
ExpressRequest,
ExpressResponse,
@@ -9,8 +10,10 @@ import {
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import logger from "../Utils/Logger";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import UserEmail from "../../Models/DatabaseModels/UserEmail";
export default class UserEmailAPI extends BaseAPI<
@@ -51,6 +54,7 @@ export default class UserEmailAPI extends BaseAPI<
},
select: {
userId: true,
projectId: true,
verificationCode: true,
},
});
@@ -94,6 +98,21 @@ export default class UserEmailAPI extends BaseAPI<
},
});
// Create default notification rules for this verified email
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(item.projectId!.toString()),
userId: new ObjectID(item.userId!.toString()),
notificationMethod: {
userEmailId: item.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);

View File

@@ -2,8 +2,10 @@ import UserMiddleware from "../Middleware/UserAuthorization";
import UserPushService, {
Service as UserPushServiceType,
} from "../Services/UserPushService";
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
import PushNotificationService from "../Services/PushNotificationService";
import PushNotificationUtil from "../Utils/PushNotificationUtil";
import logger from "../Utils/Logger";
import {
ExpressRequest,
ExpressResponse,
@@ -13,11 +15,23 @@ import {
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import ObjectID from "../../Types/ObjectID";
import PushDeviceType from "../../Types/PushNotification/PushDeviceType";
import UserPush from "../../Models/DatabaseModels/UserPush";
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
function getAuthenticatedUserId(req: ExpressRequest): ObjectID {
const userId: ObjectID | undefined = (req as OneUptimeRequest)
.userAuthorization?.userId;
if (!userId) {
throw new NotAuthenticatedException(
"You must be logged in to perform this action.",
);
}
return userId;
}
export default class UserPushAPI extends BaseAPI<
UserPush,
UserPushServiceType
@@ -32,6 +46,8 @@ export default class UserPushAPI extends BaseAPI<
try {
req = req as OneUptimeRequest;
const userId: ObjectID = getAuthenticatedUserId(req);
if (!req.body.deviceToken) {
return Response.sendErrorResponse(
req,
@@ -65,7 +81,7 @@ export default class UserPushAPI extends BaseAPI<
// Check if device is already registered
const existingDevice: UserPush | null = await this.service.findOneBy({
query: {
userId: (req as OneUptimeRequest).userAuthorization!.userId!,
userId: userId,
projectId: new ObjectID(req.body.projectId),
deviceToken: req.body.deviceToken,
},
@@ -78,17 +94,18 @@ export default class UserPushAPI extends BaseAPI<
});
if (existingDevice) {
// Mark as used and return a specific response indicating device was already registered
throw new BadDataException(
"This device is already registered for push notifications",
return Response.sendErrorResponse(
req,
res,
new BadDataException(
"This device is already registered for push notifications",
),
);
}
// Create new device registration
const userPush: UserPush = new UserPush();
userPush.userId = (
req as OneUptimeRequest
).userAuthorization!.userId!;
userPush.userId = userId;
userPush.projectId = new ObjectID(req.body.projectId);
userPush.deviceToken = req.body.deviceToken;
userPush.deviceType = req.body.deviceType;
@@ -102,6 +119,21 @@ export default class UserPushAPI extends BaseAPI<
},
});
// Create default notification rules for this registered push device
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(req.body.projectId),
userId,
notificationMethod: {
userPushId: savedDevice.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendJsonObjectResponse(req, res, {
success: true,
deviceId: savedDevice._id!.toString(),
@@ -119,6 +151,8 @@ export default class UserPushAPI extends BaseAPI<
try {
req = req as OneUptimeRequest;
const userId: ObjectID = getAuthenticatedUserId(req);
if (!req.body.deviceToken) {
return Response.sendErrorResponse(
req,
@@ -127,9 +161,6 @@ export default class UserPushAPI extends BaseAPI<
);
}
const userId: ObjectID = (req as OneUptimeRequest).userAuthorization!
.userId!;
await this.service.deleteBy({
query: {
userId: userId,
@@ -159,6 +190,8 @@ export default class UserPushAPI extends BaseAPI<
try {
req = req as OneUptimeRequest;
const userId: ObjectID = getAuthenticatedUserId(req);
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
@@ -192,10 +225,7 @@ export default class UserPushAPI extends BaseAPI<
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
if (device.userId?.toString() !== userId.toString()) {
return Response.sendErrorResponse(
req,
res,
@@ -264,6 +294,8 @@ export default class UserPushAPI extends BaseAPI<
try {
req = req as OneUptimeRequest;
const userId: ObjectID = getAuthenticatedUserId(req);
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
@@ -279,6 +311,7 @@ export default class UserPushAPI extends BaseAPI<
},
select: {
userId: true,
projectId: true,
},
});
@@ -291,10 +324,7 @@ export default class UserPushAPI extends BaseAPI<
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
if (device.userId?.toString() !== userId.toString()) {
return Response.sendErrorResponse(
req,
res,
@@ -304,6 +334,21 @@ export default class UserPushAPI extends BaseAPI<
await this.service.verifyDevice(device._id!.toString());
// Create default notification rules for this verified push device
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(device.projectId!.toString()),
userId,
notificationMethod: {
userPushId: device.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (error) {
return next(error);
@@ -318,6 +363,8 @@ export default class UserPushAPI extends BaseAPI<
try {
req = req as OneUptimeRequest;
const userId: ObjectID = getAuthenticatedUserId(req);
if (!req.params["deviceId"]) {
return Response.sendErrorResponse(
req,
@@ -345,10 +392,7 @@ export default class UserPushAPI extends BaseAPI<
}
// Check if the device belongs to the current user
if (
device.userId?.toString() !==
(req as OneUptimeRequest).userAuthorization!.userId!.toString()
) {
if (device.userId?.toString() !== userId.toString()) {
return Response.sendErrorResponse(
req,
res,

View File

@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
import UserSMSService, {
Service as UserSMSServiceType,
} from "../Services/UserSmsService";
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
import {
ExpressRequest,
ExpressResponse,
@@ -9,8 +10,10 @@ import {
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import logger from "../Utils/Logger";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import UserSMS from "../../Models/DatabaseModels/UserSMS";
export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
@@ -48,6 +51,7 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
},
select: {
userId: true,
projectId: true,
verificationCode: true,
},
});
@@ -91,6 +95,21 @@ export default class UserSMSAPI extends BaseAPI<UserSMS, UserSMSServiceType> {
},
});
// Create default notification rules for this verified SMS
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(item.projectId!.toString()),
userId: new ObjectID(item.userId!.toString()),
notificationMethod: {
userSmsId: item.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);

View File

@@ -2,6 +2,7 @@ import UserMiddleware from "../Middleware/UserAuthorization";
import UserWhatsAppService, {
Service as UserWhatsAppServiceType,
} from "../Services/UserWhatsAppService";
import UserNotificationRuleService from "../Services/UserNotificationRuleService";
import {
ExpressRequest,
ExpressResponse,
@@ -9,8 +10,10 @@ import {
OneUptimeRequest,
} from "../Utils/Express";
import Response from "../Utils/Response";
import logger from "../Utils/Logger";
import BaseAPI from "./BaseAPI";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import UserWhatsApp from "../../Models/DatabaseModels/UserWhatsApp";
export default class UserWhatsAppAPI extends BaseAPI<
@@ -50,6 +53,7 @@ export default class UserWhatsAppAPI extends BaseAPI<
},
select: {
userId: true,
projectId: true,
verificationCode: true,
isVerified: true,
},
@@ -100,6 +104,21 @@ export default class UserWhatsAppAPI extends BaseAPI<
},
});
// Create default notification rules for this verified WhatsApp number
try {
await UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
{
projectId: new ObjectID(item.projectId!.toString()),
userId: new ObjectID(item.userId!.toString()),
notificationMethod: {
userWhatsAppId: item.id!,
},
},
);
} catch (e) {
logger.error(e);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);

View File

@@ -80,4 +80,11 @@ export default class DatabaseConfig {
"disableSignup",
)) as boolean;
}
@CaptureSpan()
public static async shouldDisableUserProjectCreation(): Promise<boolean> {
return (await DatabaseConfig.getFromGlobalConfig(
"disableUserProjectCreation",
)) as boolean;
}
}

View File

@@ -529,6 +529,13 @@ export const VapidPrivateKey: string | undefined =
export const VapidSubject: string =
process.env["VAPID_SUBJECT"] || "mailto:support@oneuptime.com";
export const ExpoAccessToken: string | undefined =
process.env["EXPO_ACCESS_TOKEN"] || undefined;
export const PushNotificationRelayUrl: string =
process.env["PUSH_NOTIFICATION_RELAY_URL"] ||
"https://oneuptime.com/api/notification/push-relay/send";
export const EnterpriseLicenseValidationUrl: URL = URL.fromString(
"https://oneuptime.com/api/enterprise-license/validate",
);

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1770834237091 implements MigrationInterface {
public name = "MigrationName1770834237091";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "GlobalConfig" ADD "disableUserProjectCreation" boolean DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_disableUserProjectCreation" UNIQUE ("disableUserProjectCreation")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_disableUserProjectCreation"`,
);
await queryRunner.query(
`ALTER TABLE "GlobalConfig" DROP COLUMN "disableUserProjectCreation"`,
);
}
}

View File

@@ -258,6 +258,7 @@ import { MigrationName1770728946893 } from "./1770728946893-MigrationName";
import { MigrationName1770732721195 } from "./1770732721195-MigrationName";
import { MigrationName1770833704656 } from "./1770833704656-MigrationName";
import { MigrationName1770834237090 } from "./1770834237090-MigrationName";
import { MigrationName1770834237091 } from "./1770834237091-MigrationName";
export default [
InitialMigration,
@@ -520,4 +521,5 @@ export default [
MigrationName1770732721195,
MigrationName1770833704656,
MigrationName1770834237090,
MigrationName1770834237091,
];

View File

@@ -124,6 +124,7 @@ export class ProjectService extends DatabaseService<Model> {
select: {
name: true,
email: true,
isMasterAdmin: true,
companyPhoneNumber: true,
companyName: true,
utmCampaign: true,
@@ -142,6 +143,15 @@ export class ProjectService extends DatabaseService<Model> {
throw new BadDataException("User not found.");
}
// Check if project creation is restricted to admins only
const shouldDisableProjectCreation: boolean =
await DatabaseConfig.shouldDisableUserProjectCreation();
if (shouldDisableProjectCreation && !user.isMasterAdmin) {
throw new NotAuthorizedException(
"Project creation is restricted to admin users only on this OneUptime Server. Please contact your server admin.",
);
}
if (IsBillingEnabled) {
if (!data.data.paymentProviderPlanId) {
throw new BadDataException("Plan required to create the project.");
@@ -1313,6 +1323,8 @@ export class ProjectService extends DatabaseService<Model> {
paymentProviderSubscriptionId: true,
paymentProviderMeteredSubscriptionId: true,
name: true,
createdAt: true,
planName: true,
createdByUser: {
name: true,
email: true,
@@ -1341,6 +1353,8 @@ export class ProjectService extends DatabaseService<Model> {
let slackMessage: string = `*Project Deleted:*
*Project Name:* ${project.name?.toString() || "N/A"}
*Project ID:* ${project._id?.toString() || "N/A"}
*Project Created Date:* ${project.createdAt ? new Date(project.createdAt).toUTCString() : "N/A"}
*Project Plan Name:* ${project.planName?.toString() || "N/A"}
`;
if (subscriptionStatus) {

View File

@@ -10,9 +10,16 @@ import {
VapidPublicKey,
VapidPrivateKey,
VapidSubject,
ExpoAccessToken,
PushNotificationRelayUrl,
} from "../EnvironmentConfig";
import webpush from "web-push";
import { Expo, ExpoPushMessage, ExpoPushTicket } from "expo-server-sdk";
import API from "../../Utils/API";
import URL from "../../Types/API/URL";
import HTTPErrorResponse from "../../Types/API/HTTPErrorResponse";
import HTTPResponse from "../../Types/API/HTTPResponse";
import { JSONObject } from "../../Types/JSON";
import PushNotificationUtil from "../Utils/PushNotificationUtil";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import UserPush from "../../Models/DatabaseModels/UserPush";
@@ -43,7 +50,9 @@ export interface PushNotificationOptions {
export default class PushNotificationService {
public static isWebPushInitialized = false;
private static expoClient: Expo = new Expo();
private static expoClient: Expo = new Expo(
ExpoAccessToken ? { accessToken: ExpoAccessToken } : undefined,
);
public static initializeWebPush(): void {
if (this.isWebPushInitialized) {
@@ -340,20 +349,33 @@ export default class PushNotificationService {
);
}
const dataPayload: { [key: string]: string } = {};
if (message.data) {
for (const key of Object.keys(message.data)) {
dataPayload[key] = String(message.data[key]);
}
}
if (message.url || message.clickAction) {
dataPayload["url"] = message.url || message.clickAction || "";
}
const channelId: string =
deviceType === PushDeviceType.Android ? "oncall_high" : "default";
// If EXPO_ACCESS_TOKEN is not set, relay through the push notification gateway
if (!ExpoAccessToken) {
await this.sendViaRelay(
expoPushToken,
message,
dataPayload,
channelId,
deviceType,
);
return;
}
// Send directly via Expo SDK
try {
const dataPayload: { [key: string]: string } = {};
if (message.data) {
for (const key of Object.keys(message.data)) {
dataPayload[key] = String(message.data[key]);
}
}
if (message.url || message.clickAction) {
dataPayload["url"] = message.url || message.clickAction || "";
}
const channelId: string =
deviceType === PushDeviceType.Android ? "oncall_high" : "default";
const expoPushMessage: ExpoPushMessage = {
to: expoPushToken,
title: message.title,
@@ -403,6 +425,109 @@ export default class PushNotificationService {
}
}
private static async sendViaRelay(
expoPushToken: string,
message: PushNotificationMessage,
dataPayload: { [key: string]: string },
channelId: string,
deviceType: PushDeviceType,
): Promise<void> {
logger.info(
`Sending ${deviceType} push notification via relay: ${PushNotificationRelayUrl}`,
);
try {
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post<JSONObject>({
url: URL.fromString(PushNotificationRelayUrl),
data: {
to: expoPushToken,
title: message.title || "",
body: message.body || "",
data: dataPayload,
sound: "default",
priority: "high",
channelId: channelId,
},
});
if (response instanceof HTTPErrorResponse) {
throw new Error(
`Push relay error: ${JSON.stringify(response.jsonData)}`,
);
}
logger.info(
`Push notification sent via relay successfully to ${deviceType} device`,
);
} catch (error: any) {
logger.error(
`Failed to send push notification via relay to ${deviceType} device: ${error.message}`,
);
throw error;
}
}
public static isValidExpoPushToken(token: string): boolean {
return Expo.isExpoPushToken(token);
}
public static hasExpoAccessToken(): boolean {
return Boolean(ExpoAccessToken);
}
public static async sendRelayPushNotification(data: {
to: string;
title?: string;
body?: string;
data?: { [key: string]: string };
sound?: string;
priority?: string;
channelId?: string;
}): Promise<void> {
if (!ExpoAccessToken) {
throw new Error(
"Push relay is not configured. EXPO_ACCESS_TOKEN is not set on this server.",
);
}
const expoPushMessage: ExpoPushMessage = {
to: data.to,
title: data.title || "",
body: data.body || "",
data: data.data || {},
sound: (data.sound as "default" | null) || "default",
priority:
(data.priority as "default" | "normal" | "high") || "high",
channelId: data.channelId || "default",
};
const tickets: ExpoPushTicket[] =
await this.expoClient.sendPushNotificationsAsync([expoPushMessage]);
const ticket: ExpoPushTicket | undefined = tickets[0];
if (ticket && ticket.status === "error") {
const errorTicket: ExpoPushTicket & {
message?: string;
details?: { error?: string };
} = ticket as ExpoPushTicket & {
message?: string;
details?: { error?: string };
};
logger.error(
`Push relay: Expo push notification error: ${errorTicket.message}`,
);
throw new Error(
`Failed to send push notification: ${errorTicket.message}`,
);
}
logger.info(`Push relay: notification sent successfully to ${data.to}`);
}
public static async sendPushNotificationToUser(
userId: ObjectID,
projectId: ObjectID,

View File

@@ -69,6 +69,14 @@ import PushNotificationMessage from "../../Types/PushNotification/PushNotificati
import logger from "../Utils/Logger";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
export interface NotificationMethodDescriptor {
userEmailId?: ObjectID;
userSmsId?: ObjectID;
userCallId?: ObjectID;
userWhatsAppId?: ObjectID;
userPushId?: ObjectID;
}
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
@@ -2207,13 +2215,89 @@ export class Service extends DatabaseService<Model> {
}
@CaptureSpan()
public async addDefaultIncidentNotificationRuleForUser(data: {
public async addDefaultNotificationRulesForVerifiedMethod(data: {
projectId: ObjectID;
userId: ObjectID;
userEmail: UserEmail;
notificationMethod: NotificationMethodDescriptor;
}): Promise<void> {
const { projectId, userId, userEmail } = data;
const { projectId, userId, notificationMethod } = data;
await this.createIncidentOnCallRules(projectId, userId, notificationMethod);
await this.createAlertOnCallRules(projectId, userId, notificationMethod);
await this.createSingleRule(
projectId,
userId,
notificationMethod,
NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
);
await this.createSingleRule(
projectId,
userId,
notificationMethod,
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
);
await this.createSingleRule(
projectId,
userId,
notificationMethod,
NotificationRuleType.WHEN_USER_GOES_ON_CALL,
);
await this.createSingleRule(
projectId,
userId,
notificationMethod,
NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
);
}
private applyNotificationMethod(
rule: Model,
descriptor: NotificationMethodDescriptor,
): void {
if (descriptor.userEmailId) {
rule.userEmailId = descriptor.userEmailId;
}
if (descriptor.userSmsId) {
rule.userSmsId = descriptor.userSmsId;
}
if (descriptor.userCallId) {
rule.userCallId = descriptor.userCallId;
}
if (descriptor.userWhatsAppId) {
rule.userWhatsAppId = descriptor.userWhatsAppId;
}
if (descriptor.userPushId) {
rule.userPushId = descriptor.userPushId;
}
}
private getNotificationMethodQuery(
descriptor: NotificationMethodDescriptor,
): Record<string, ObjectID> {
const query: Record<string, ObjectID> = {};
if (descriptor.userEmailId) {
query["userEmailId"] = descriptor.userEmailId;
}
if (descriptor.userSmsId) {
query["userSmsId"] = descriptor.userSmsId;
}
if (descriptor.userCallId) {
query["userCallId"] = descriptor.userCallId;
}
if (descriptor.userWhatsAppId) {
query["userWhatsAppId"] = descriptor.userWhatsAppId;
}
if (descriptor.userPushId) {
query["userPushId"] = descriptor.userPushId;
}
return query;
}
private async createIncidentOnCallRules(
projectId: ObjectID,
userId: ObjectID,
notificationMethod: NotificationMethodDescriptor,
): Promise<void> {
const incidentSeverities: Array<IncidentSeverity> =
await IncidentSeverityService.findBy({
query: {
@@ -2229,38 +2313,34 @@ export class Service extends DatabaseService<Model> {
},
});
// create for incident severities.
for (const incidentSeverity of incidentSeverities) {
//check if this rule already exists.
const existingRule: Model | null = await this.findOneBy({
query: {
projectId,
userId,
userEmailId: userEmail.id!,
...this.getNotificationMethodQuery(notificationMethod),
incidentSeverityId: incidentSeverity.id!,
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
},
} as any,
props: {
isRoot: true,
},
});
if (existingRule) {
continue; // skip this rule.
continue;
}
const notificationRule: Model = new Model();
notificationRule.projectId = projectId;
notificationRule.userId = userId;
notificationRule.userEmailId = userEmail.id!;
notificationRule.incidentSeverityId = incidentSeverity.id!;
notificationRule.notifyAfterMinutes = 0;
notificationRule.ruleType =
NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
const rule: Model = new Model();
rule.projectId = projectId;
rule.userId = userId;
this.applyNotificationMethod(rule, notificationMethod);
rule.incidentSeverityId = incidentSeverity.id!;
rule.notifyAfterMinutes = 0;
rule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
await this.create({
data: notificationRule,
data: rule,
props: {
isRoot: true,
},
@@ -2268,14 +2348,11 @@ export class Service extends DatabaseService<Model> {
}
}
@CaptureSpan()
public async addDefaultAlertNotificationRulesForUser(data: {
projectId: ObjectID;
userId: ObjectID;
userEmail: UserEmail;
}): Promise<void> {
const { projectId, userId, userEmail } = data;
private async createAlertOnCallRules(
projectId: ObjectID,
userId: ObjectID,
notificationMethod: NotificationMethodDescriptor,
): Promise<void> {
const alertSeverities: Array<AlertSeverity> =
await AlertSeverityService.findBy({
query: {
@@ -2291,37 +2368,34 @@ export class Service extends DatabaseService<Model> {
},
});
// create for Alert severities.
for (const alertSeverity of alertSeverities) {
//check if this rule already exists.
const existingRule: Model | null = await this.findOneBy({
query: {
projectId,
userId,
userEmailId: userEmail.id!,
...this.getNotificationMethodQuery(notificationMethod),
alertSeverityId: alertSeverity.id!,
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
},
} as any,
props: {
isRoot: true,
},
});
if (existingRule) {
continue; // skip this rule.
continue;
}
const notificationRule: Model = new Model();
notificationRule.projectId = projectId;
notificationRule.userId = userId;
notificationRule.userEmailId = userEmail.id!;
notificationRule.alertSeverityId = alertSeverity.id!;
notificationRule.notifyAfterMinutes = 0;
notificationRule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_ALERT;
const rule: Model = new Model();
rule.projectId = projectId;
rule.userId = userId;
this.applyNotificationMethod(rule, notificationMethod);
rule.alertSeverityId = alertSeverity.id!;
rule.notifyAfterMinutes = 0;
rule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_ALERT;
await this.create({
data: notificationRule,
data: rule,
props: {
isRoot: true,
},
@@ -2329,6 +2403,43 @@ export class Service extends DatabaseService<Model> {
}
}
private async createSingleRule(
projectId: ObjectID,
userId: ObjectID,
notificationMethod: NotificationMethodDescriptor,
ruleType: NotificationRuleType,
): Promise<void> {
const existingRule: Model | null = await this.findOneBy({
query: {
projectId,
userId,
...this.getNotificationMethodQuery(notificationMethod),
ruleType,
} as any,
props: {
isRoot: true,
},
});
if (existingRule) {
return;
}
const rule: Model = new Model();
rule.projectId = projectId;
rule.userId = userId;
this.applyNotificationMethod(rule, notificationMethod);
rule.notifyAfterMinutes = 0;
rule.ruleType = ruleType;
await this.create({
data: rule,
props: {
isRoot: true,
},
});
}
@CaptureSpan()
public async addDefaultNotificationRuleForUser(
projectId: ObjectID,
@@ -2361,82 +2472,13 @@ export class Service extends DatabaseService<Model> {
});
}
// add default incident rules for user
await this.addDefaultIncidentNotificationRuleForUser({
await this.addDefaultNotificationRulesForVerifiedMethod({
projectId,
userId,
userEmail,
});
// add default alert rules for user, just like the incident
await this.addDefaultAlertNotificationRulesForUser({
projectId,
userId,
userEmail,
});
//check if this rule already exists.
const existingRuleOnCall: Model | null = await this.findOneBy({
query: {
projectId,
userId,
notificationMethod: {
userEmailId: userEmail.id!,
ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
},
props: {
isRoot: true,
},
});
if (!existingRuleOnCall) {
// on and off call.
const onCallRule: Model = new Model();
onCallRule.projectId = projectId;
onCallRule.userId = userId;
onCallRule.userEmailId = userEmail.id!;
onCallRule.notifyAfterMinutes = 0;
onCallRule.ruleType = NotificationRuleType.WHEN_USER_GOES_ON_CALL;
await this.create({
data: onCallRule,
props: {
isRoot: true,
},
});
}
//check if this rule already exists.
const existingRuleOffCall: Model | null = await this.findOneBy({
query: {
projectId,
userId,
userEmailId: userEmail.id!,
ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
},
props: {
isRoot: true,
},
});
if (!existingRuleOffCall) {
// on and off call.
const offCallRule: Model = new Model();
offCallRule.projectId = projectId;
offCallRule.userId = userId;
offCallRule.userEmailId = userEmail.id!;
offCallRule.notifyAfterMinutes = 0;
offCallRule.ruleType = NotificationRuleType.WHEN_USER_GOES_OFF_CALL;
await this.create({
data: offCallRule,
props: {
isRoot: true,
},
});
}
}
}
export default new Service();

View File

@@ -0,0 +1,142 @@
import DataToProcess from "../DataToProcess";
import CompareCriteria from "./CompareCriteria";
import {
CheckOn,
CriteriaFilter,
FilterType,
} from "../../../../Types/Monitor/CriteriaFilter";
import DomainMonitorResponse from "../../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
import CaptureSpan from "../../Telemetry/CaptureSpan";
export default class DomainMonitorCriteria {
@CaptureSpan()
public static async isMonitorInstanceCriteriaFilterMet(input: {
dataToProcess: DataToProcess;
criteriaFilter: CriteriaFilter;
}): Promise<string | null> {
let threshold: number | string | undefined | null =
input.criteriaFilter.value;
const dataToProcess: ProbeMonitorResponse =
input.dataToProcess as ProbeMonitorResponse;
const domainResponse: DomainMonitorResponse | undefined =
dataToProcess.domainResponse;
// Check domain expires in days
if (input.criteriaFilter.checkOn === CheckOn.DomainExpiresDaysIn) {
threshold = CompareCriteria.convertToNumber(threshold);
if (threshold === null || threshold === undefined) {
return null;
}
if (!domainResponse?.expiresDate) {
return null;
}
const expiresDate: Date = new Date(domainResponse.expiresDate);
const now: Date = new Date();
const diffMs: number = expiresDate.getTime() - now.getTime();
const diffDays: number = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
return CompareCriteria.compareCriteriaNumbers({
value: diffDays,
threshold: threshold as number,
criteriaFilter: input.criteriaFilter,
});
}
// Check domain registrar
if (input.criteriaFilter.checkOn === CheckOn.DomainRegistrar) {
if (!domainResponse?.registrar) {
return null;
}
return CompareCriteria.compareCriteriaStrings({
value: domainResponse.registrar,
threshold: String(threshold),
criteriaFilter: input.criteriaFilter,
});
}
// Check domain name server
if (input.criteriaFilter.checkOn === CheckOn.DomainNameServer) {
if (
!domainResponse?.nameServers ||
domainResponse.nameServers.length === 0
) {
return null;
}
// Check if any name server matches the criteria
for (const nameServer of domainResponse.nameServers) {
const result: string | null = CompareCriteria.compareCriteriaStrings({
value: nameServer,
threshold: String(threshold),
criteriaFilter: input.criteriaFilter,
});
if (result) {
return `Domain name server: ${result}`;
}
}
return null;
}
// Check domain status code
if (input.criteriaFilter.checkOn === CheckOn.DomainStatusCode) {
if (
!domainResponse?.domainStatus ||
domainResponse.domainStatus.length === 0
) {
return null;
}
// Check if any status matches the criteria
for (const status of domainResponse.domainStatus) {
const result: string | null = CompareCriteria.compareCriteriaStrings({
value: status,
threshold: String(threshold),
criteriaFilter: input.criteriaFilter,
});
if (result) {
return `Domain status: ${result}`;
}
}
return null;
}
// Check if domain is expired
if (input.criteriaFilter.checkOn === CheckOn.DomainIsExpired) {
const isTrue: boolean =
input.criteriaFilter.filterType === FilterType.True;
const isFalse: boolean =
input.criteriaFilter.filterType === FilterType.False;
if (!domainResponse?.expiresDate) {
return null;
}
const expiresDate: Date = new Date(domainResponse.expiresDate);
const now: Date = new Date();
const isExpired: boolean = expiresDate.getTime() < now.getTime();
if (isExpired && isTrue) {
return `Domain is expired (expired on ${domainResponse.expiresDate}).`;
}
if (!isExpired && isFalse) {
return `Domain is not expired (expires on ${domainResponse.expiresDate}).`;
}
return null;
}
return null;
}
}

View File

@@ -13,6 +13,7 @@ import TraceMonitorCriteria from "./Criteria/TraceMonitorCriteria";
import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
import DomainMonitorCriteria from "./Criteria/DomainMonitorCriteria";
import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
@@ -506,6 +507,18 @@ ${contextBlock}
}
}
if (input.monitor.monitorType === MonitorType.Domain) {
const domainMonitorResult: string | null =
await DomainMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
dataToProcess: input.dataToProcess,
criteriaFilter: input.criteriaFilter,
});
if (domainMonitorResult) {
return domainMonitorResult;
}
}
return null;
}

View File

@@ -17,6 +17,7 @@ import SnmpMonitorResponse, {
import DnsMonitorResponse, {
DnsRecordResponse,
} from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
import DomainMonitorResponse from "../../../Types/Monitor/DomainMonitor/DomainMonitorResponse";
import Typeof from "../../../Types/Typeof";
import VMUtil from "../VM/VMAPI";
import DataToProcess from "./DataToProcess";
@@ -277,6 +278,26 @@ export default class MonitorTemplateUtil {
);
}
}
if (data.monitorType === MonitorType.Domain) {
const domainResponse: DomainMonitorResponse | undefined = (
data.dataToProcess as ProbeMonitorResponse
).domainResponse;
storageMap = {
isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline,
responseTimeInMs: domainResponse?.responseTimeInMs,
failureCause: domainResponse?.failureCause,
domainName: domainResponse?.domainName,
registrar: domainResponse?.registrar,
createdDate: domainResponse?.createdDate,
updatedDate: domainResponse?.updatedDate,
expiresDate: domainResponse?.expiresDate,
nameServers: domainResponse?.nameServers,
domainStatus: domainResponse?.domainStatus,
dnssec: domainResponse?.dnssec,
} as JSONObject;
}
} catch (err) {
logger.error(err);
}

View File

@@ -67,6 +67,13 @@ export enum CheckOn {
DnsRecordValue = "DNS Record Value",
DnssecIsValid = "DNSSEC Is Valid",
DnsRecordExists = "DNS Record Exists",
// Domain monitors.
DomainExpiresDaysIn = "Domain Expires In Days",
DomainRegistrar = "Domain Registrar",
DomainNameServer = "Domain Name Server",
DomainStatusCode = "Domain Status Code",
DomainIsExpired = "Domain Is Expired",
}
export interface ServerMonitorOptions {
@@ -151,7 +158,8 @@ export class CriteriaFilterUtil {
if (
checkOn === CheckOn.IsOnline ||
checkOn === CheckOn.SnmpIsOnline ||
checkOn === CheckOn.DnsIsOnline
checkOn === CheckOn.DnsIsOnline ||
checkOn === CheckOn.DomainIsExpired
) {
return false;
}

View File

@@ -0,0 +1,15 @@
export default interface DomainMonitorResponse {
isOnline: boolean;
responseTimeInMs: number;
failureCause: string;
domainName: string;
registrar?: string | undefined;
registrarUrl?: string | undefined;
createdDate?: string | undefined;
updatedDate?: string | undefined;
expiresDate?: string | undefined;
nameServers?: Array<string> | undefined;
dnssec?: string | undefined;
domainStatus?: Array<string> | undefined;
isTimeout?: boolean | undefined;
}

View File

@@ -421,6 +421,33 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
return monitorCriteriaInstance;
}
if (arg.monitorType === MonitorType.Domain) {
const monitorCriteriaInstance: MonitorCriteriaInstance =
new MonitorCriteriaInstance();
monitorCriteriaInstance.data = {
id: ObjectID.generate().toString(),
monitorStatusId: arg.monitorStatusId,
filterCondition: FilterCondition.All,
filters: [
{
checkOn: CheckOn.DomainIsExpired,
filterType: FilterType.False,
value: undefined,
},
],
incidents: [],
alerts: [],
createAlerts: false,
changeMonitorStatus: true,
createIncidents: false,
name: `Check if ${arg.monitorName} is not expired`,
description: `This criteria checks if the ${arg.monitorName} domain registration is not expired`,
};
return monitorCriteriaInstance;
}
return null;
}
@@ -562,6 +589,46 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
};
}
if (arg.monitorType === MonitorType.Domain) {
monitorCriteriaInstance.data = {
id: ObjectID.generate().toString(),
monitorStatusId: arg.monitorStatusId,
filterCondition: FilterCondition.Any,
filters: [
{
checkOn: CheckOn.DomainIsExpired,
filterType: FilterType.True,
value: undefined,
},
],
incidents: [
{
title: `${arg.monitorName} domain is expired`,
description: `${arg.monitorName} domain registration has expired.`,
incidentSeverityId: arg.incidentSeverityId,
autoResolveIncident: true,
id: ObjectID.generate().toString(),
onCallPolicyIds: [],
},
],
changeMonitorStatus: true,
createIncidents: true,
createAlerts: false,
alerts: [
{
title: `${arg.monitorName} domain is expired`,
description: `${arg.monitorName} domain registration has expired.`,
alertSeverityId: arg.alertSeverityId,
autoResolveAlert: true,
id: ObjectID.generate().toString(),
onCallPolicyIds: [],
},
],
name: `Check if ${arg.monitorName} domain is expired`,
description: `This criteria checks if the ${arg.monitorName} domain registration has expired`,
};
}
if (
arg.monitorType === MonitorType.API ||
arg.monitorType === MonitorType.Website

View File

@@ -32,6 +32,9 @@ import MonitorStepSnmpMonitor, {
import MonitorStepDnsMonitor, {
MonitorStepDnsMonitorUtil,
} from "./MonitorStepDnsMonitor";
import MonitorStepDomainMonitor, {
MonitorStepDomainMonitorUtil,
} from "./MonitorStepDomainMonitor";
import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
export interface MonitorStepType {
@@ -78,6 +81,9 @@ export interface MonitorStepType {
// DNS monitor
dnsMonitor?: MonitorStepDnsMonitor | undefined;
// Domain monitor
domainMonitor?: MonitorStepDomainMonitor | undefined;
}
export default class MonitorStep extends DatabaseProperty {
@@ -105,6 +111,7 @@ export default class MonitorStep extends DatabaseProperty {
exceptionMonitor: undefined,
snmpMonitor: undefined,
dnsMonitor: undefined,
domainMonitor: undefined,
};
}
@@ -137,6 +144,7 @@ export default class MonitorStep extends DatabaseProperty {
exceptionMonitor: undefined,
snmpMonitor: undefined,
dnsMonitor: undefined,
domainMonitor: undefined,
};
return monitorStep;
@@ -237,6 +245,13 @@ export default class MonitorStep extends DatabaseProperty {
return this;
}
public setDomainMonitor(
domainMonitor: MonitorStepDomainMonitor,
): MonitorStep {
this.data!.domainMonitor = domainMonitor;
return this;
}
public setCustomCode(customCode: string): MonitorStep {
this.data!.customCode = customCode;
return this;
@@ -355,6 +370,16 @@ export default class MonitorStep extends DatabaseProperty {
}
}
if (monitorType === MonitorType.Domain) {
if (!value.data.domainMonitor) {
return "Domain configuration is required";
}
if (!value.data.domainMonitor.domainName) {
return "Domain name is required";
}
}
return null;
}
@@ -403,6 +428,9 @@ export default class MonitorStep extends DatabaseProperty {
dnsMonitor: this.data.dnsMonitor
? MonitorStepDnsMonitorUtil.toJSON(this.data.dnsMonitor)
: undefined,
domainMonitor: this.data.domainMonitor
? MonitorStepDomainMonitorUtil.toJSON(this.data.domainMonitor)
: undefined,
},
});
}
@@ -511,6 +539,9 @@ export default class MonitorStep extends DatabaseProperty {
dnsMonitor: json["dnsMonitor"]
? (json["dnsMonitor"] as JSONObject)
: undefined,
domainMonitor: json["domainMonitor"]
? (json["domainMonitor"] as JSONObject)
: undefined,
}) as any;
return monitorStep;
@@ -537,6 +568,7 @@ export default class MonitorStep extends DatabaseProperty {
metricMonitor: Zod.any().optional(),
snmpMonitor: Zod.any().optional(),
dnsMonitor: Zod.any().optional(),
domainMonitor: Zod.any().optional(),
}).openapi({
type: "object",
example: {

View File

@@ -0,0 +1,33 @@
import { JSONObject } from "../JSON";
export default interface MonitorStepDomainMonitor {
domainName: string;
timeout: number;
retries: number;
}
export class MonitorStepDomainMonitorUtil {
public static getDefault(): MonitorStepDomainMonitor {
return {
domainName: "",
timeout: 10000,
retries: 3,
};
}
public static fromJSON(json: JSONObject): MonitorStepDomainMonitor {
return {
domainName: (json["domainName"] as string) || "",
timeout: (json["timeout"] as number) || 10000,
retries: (json["retries"] as number) || 3,
};
}
public static toJSON(monitor: MonitorStepDomainMonitor): JSONObject {
return {
domainName: monitor.domainName,
timeout: monitor.timeout,
retries: monitor.retries,
};
}
}

View File

@@ -29,6 +29,9 @@ enum MonitorType {
// DNS monitoring
DNS = "DNS",
// Domain registration monitoring
Domain = "Domain",
}
export default MonitorType;
@@ -40,7 +43,58 @@ export interface MonitorTypeProps {
icon: IconProp;
}
export interface MonitorTypeCategory {
label: string;
monitorTypes: Array<MonitorType>;
}
export class MonitorTypeHelper {
public static getMonitorTypeCategories(): Array<MonitorTypeCategory> {
return [
{
label: "Basic Monitoring",
monitorTypes: [
MonitorType.Website,
MonitorType.API,
MonitorType.Ping,
MonitorType.IP,
MonitorType.Port,
MonitorType.DNS,
MonitorType.SSLCertificate,
MonitorType.Domain,
],
},
{
label: "Synthetic Monitoring",
monitorTypes: [
MonitorType.SyntheticMonitor,
MonitorType.CustomJavaScriptCode,
],
},
{
label: "Inbound Monitoring",
monitorTypes: [MonitorType.IncomingRequest, MonitorType.IncomingEmail],
},
{
label: "Infrastructure",
monitorTypes: [MonitorType.Server, MonitorType.SNMP],
},
{
label: "Telemetry",
monitorTypes: [
MonitorType.Logs,
MonitorType.Metrics,
MonitorType.Traces,
MonitorType.Exceptions,
],
},
{
label: "Other",
monitorTypes: [MonitorType.Manual],
},
];
}
public static isTelemetryMonitor(monitorType: MonitorType): boolean {
return (
monitorType === MonitorType.Logs ||
@@ -189,6 +243,13 @@ export class MonitorTypeHelper {
"This monitor type lets you monitor DNS resolution for your domains, verify record values, and check DNSSEC validity.",
icon: IconProp.GlobeAlt,
},
{
monitorType: MonitorType.Domain,
title: "Domain",
description:
"This monitor type lets you monitor domain registration health — expiry dates, registrar info, nameserver delegation, and WHOIS status.",
icon: IconProp.Globe,
},
];
return monitorTypeProps;
@@ -235,7 +296,8 @@ export class MonitorTypeHelper {
monitorType === MonitorType.SyntheticMonitor ||
monitorType === MonitorType.CustomJavaScriptCode ||
monitorType === MonitorType.SNMP ||
monitorType === MonitorType.DNS;
monitorType === MonitorType.DNS ||
monitorType === MonitorType.Domain;
return isProbeableMonitor;
}
@@ -258,6 +320,7 @@ export class MonitorTypeHelper {
MonitorType.Exceptions,
MonitorType.SNMP,
MonitorType.DNS,
MonitorType.Domain,
];
}
@@ -291,7 +354,8 @@ export class MonitorTypeHelper {
monitorType === MonitorType.SyntheticMonitor ||
monitorType === MonitorType.CustomJavaScriptCode ||
monitorType === MonitorType.SNMP ||
monitorType === MonitorType.DNS
monitorType === MonitorType.DNS ||
monitorType === MonitorType.Domain
) {
return true;
}

View File

@@ -8,6 +8,7 @@ import SslMonitorResponse from "../Monitor/SSLMonitor/SslMonitorResponse";
import SyntheticMonitorResponse from "../Monitor/SyntheticMonitors/SyntheticMonitorResponse";
import SnmpMonitorResponse from "../Monitor/SnmpMonitor/SnmpMonitorResponse";
import DnsMonitorResponse from "../Monitor/DnsMonitor/DnsMonitorResponse";
import DomainMonitorResponse from "../Monitor/DomainMonitor/DomainMonitorResponse";
import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
import ObjectID from "../ObjectID";
import Port from "../Port";
@@ -32,6 +33,7 @@ export default interface ProbeMonitorResponse {
customCodeMonitorResponse?: CustomCodeMonitorResponse | undefined;
snmpResponse?: SnmpMonitorResponse | undefined;
dnsResponse?: DnsMonitorResponse | undefined;
domainResponse?: DomainMonitorResponse | undefined;
monitoredAt: Date;
isTimeout?: boolean | undefined;
ingestedAt?: Date | undefined;

View File

@@ -9,8 +9,22 @@ export interface CardSelectOption {
icon: IconProp;
}
export interface ComponentProps {
export interface CardSelectOptionGroup {
label: string;
options: Array<CardSelectOption>;
}
export function isCardSelectOptionGroup(
option: CardSelectOption | CardSelectOptionGroup,
): option is CardSelectOptionGroup {
return (
(option as CardSelectOptionGroup).label !== undefined &&
Array.isArray((option as CardSelectOptionGroup).options)
);
}
export interface ComponentProps {
options: Array<CardSelectOption | CardSelectOptionGroup>;
value?: string | undefined;
onChange: (value: string) => void;
error?: string | undefined;
@@ -18,80 +32,132 @@ export interface ComponentProps {
dataTestId?: string | undefined;
}
interface RenderGroup {
label: string | null;
options: Array<CardSelectOption>;
}
const CardSelect: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
// Normalize options into render groups
const groups: Array<RenderGroup> = [];
let ungroupedOptions: Array<CardSelectOption> = [];
for (const option of props.options) {
if (isCardSelectOptionGroup(option)) {
// Flush any accumulated ungrouped options first
if (ungroupedOptions.length > 0) {
groups.push({ label: null, options: ungroupedOptions });
ungroupedOptions = [];
}
groups.push({ label: option.label, options: option.options });
} else {
ungroupedOptions.push(option);
}
}
if (ungroupedOptions.length > 0) {
groups.push({ label: null, options: ungroupedOptions });
}
let cardIndex: number = 0;
return (
<div data-testid={props.dataTestId}>
<div
role="radiogroup"
aria-label="Select an option"
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
{props.options.map((option: CardSelectOption, index: number) => {
const isSelected: boolean = props.value === option.value;
<div role="radiogroup" aria-label="Select an option">
{groups.map((group: RenderGroup, groupIndex: number) => {
return (
<div
key={index}
tabIndex={props.tabIndex ? props.tabIndex + index : index}
onClick={() => {
props.onChange(option.value);
}}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
props.onChange(option.value);
}
}}
className={`relative flex cursor-pointer rounded-lg border p-4 shadow-sm focus:outline-none transition-all duration-200 hover:border-indigo-400 hover:shadow-md ${
isSelected
? "border-indigo-500 bg-indigo-50/50"
: "border-gray-200 bg-white"
}`}
role="radio"
aria-checked={isSelected}
data-testid={`card-select-option-${option.value}`}
>
<div className="flex w-full items-start">
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
isSelected ? "bg-indigo-100" : "bg-gray-100"
}`}
>
<Icon
icon={option.icon}
size={SizeProp.Large}
className={`h-5 w-5 ${
isSelected ? "text-indigo-600" : "text-gray-600"
}`}
/>
</div>
<div className="ml-4 flex-1">
<span
className={`block text-sm font-semibold ${
isSelected ? "text-gray-900" : "text-gray-900"
}`}
<div key={groupIndex} className={groupIndex > 0 ? "mt-8" : ""}>
{group.label && (
<div className="relative mb-4">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
{option.title}
</span>
<span
className={`mt-1 block text-sm ${
isSelected ? "text-gray-600" : "text-gray-500"
}`}
>
{option.description}
</span>
</div>
{isSelected && (
<div className="flex-shrink-0 ml-2">
<Icon
icon={IconProp.CheckCircle}
size={SizeProp.Large}
className="h-5 w-5 text-indigo-500"
/>
<div className="w-full border-t border-gray-200"></div>
</div>
)}
<div className="relative flex justify-start">
<span className="bg-white pr-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
{group.label}
</span>
</div>
</div>
)}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{group.options.map((option: CardSelectOption) => {
const isSelected: boolean = props.value === option.value;
const currentIndex: number = cardIndex++;
return (
<div
key={option.value}
tabIndex={
props.tabIndex
? props.tabIndex + currentIndex
: currentIndex
}
onClick={() => {
props.onChange(option.value);
}}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
props.onChange(option.value);
}
}}
className={`relative flex cursor-pointer rounded-lg border p-4 shadow-sm focus:outline-none transition-all duration-200 hover:border-indigo-400 hover:shadow-md ${
isSelected
? "border-indigo-500 bg-indigo-50/50"
: "border-gray-200 bg-white"
}`}
role="radio"
aria-checked={isSelected}
data-testid={`card-select-option-${option.value}`}
>
<div className="flex w-full items-start">
<div
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg ${
isSelected ? "bg-indigo-100" : "bg-gray-100"
}`}
>
<Icon
icon={option.icon}
size={SizeProp.Large}
className={`h-5 w-5 ${
isSelected ? "text-indigo-600" : "text-gray-600"
}`}
/>
</div>
<div className="ml-4 flex-1">
<span
className={`block text-sm font-semibold ${
isSelected ? "text-gray-900" : "text-gray-900"
}`}
>
{option.title}
</span>
<span
className={`mt-1 block text-sm ${
isSelected ? "text-gray-600" : "text-gray-500"
}`}
>
{option.description}
</span>
</div>
{isSelected && (
<div className="flex-shrink-0 ml-2">
<Icon
icon={IconProp.CheckCircle}
size={SizeProp.Large}
className="h-5 w-5 text-indigo-500"
/>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);

View File

@@ -3,7 +3,10 @@ import {
CategoryCheckboxOption,
CheckboxCategory,
} from "../../CategoryCheckbox/CategoryCheckboxTypes";
import { CardSelectOption } from "../../CardSelect/CardSelect";
import {
CardSelectOption,
CardSelectOptionGroup,
} from "../../CardSelect/CardSelect";
import { DropdownOption, DropdownOptionGroup } from "../../Dropdown/Dropdown";
import { RadioButton } from "../../RadioButtons/GroupRadioButtons";
import FormFieldSchemaType from "./FormFieldSchemaType";
@@ -51,7 +54,9 @@ export default interface Field<TEntity> {
stepId?: string | undefined;
required?: boolean | ((item: FormValues<TEntity>) => boolean) | undefined;
dropdownOptions?: Array<DropdownOption | DropdownOptionGroup> | undefined;
cardSelectOptions?: Array<CardSelectOption> | undefined;
cardSelectOptions?:
| Array<CardSelectOption | CardSelectOptionGroup>
| undefined;
fetchDropdownOptions?:
| ((
item: FormValues<TEntity>,

26
Common/UI/index.d.ts vendored
View File

@@ -2,3 +2,29 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpg";
declare module "*.gif";
declare module "react-syntax-highlighter/dist/esm/prism-light";
declare module "react-syntax-highlighter/dist/esm/styles/prism";
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";

115
Common/package-lock.json generated
View File

@@ -930,54 +930,42 @@
}
},
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
"integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "11.0.3",
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/gast": "11.1.1",
"@chevrotain/types": "11.1.1",
"lodash-es": "4.17.23"
}
},
"node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/@chevrotain/gast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
"integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/types": "11.1.1",
"lodash-es": "4.17.23"
}
},
"node_modules/@chevrotain/gast/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/@chevrotain/regexp-to-ast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
"integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/types": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
"integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
"integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
"license": "Apache-2.0"
},
"node_modules/@clickhouse/client": {
@@ -2200,12 +2188,12 @@
"license": "MIT"
},
"node_modules/@mermaid-js/parser": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
"integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==",
"license": "MIT",
"dependencies": {
"langium": "3.3.1"
"langium": "^4.0.0"
}
},
"node_modules/@monaco-editor/loader": {
@@ -7073,17 +7061,17 @@
}
},
"node_modules/chevrotain": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
"@chevrotain/regexp-to-ast": "11.0.3",
"@chevrotain/types": "11.0.3",
"@chevrotain/utils": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/cst-dts-gen": "11.1.1",
"@chevrotain/gast": "11.1.1",
"@chevrotain/regexp-to-ast": "11.1.1",
"@chevrotain/types": "11.1.1",
"@chevrotain/utils": "11.1.1",
"lodash-es": "4.17.23"
}
},
"node_modules/chevrotain-allstar": {
@@ -7098,12 +7086,6 @@
"chevrotain": "^11.0.0"
}
},
"node_modules/chevrotain/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -12177,19 +12159,20 @@
}
},
"node_modules/langium": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz",
"integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==",
"license": "MIT",
"dependencies": {
"chevrotain": "~11.0.3",
"chevrotain-allstar": "~0.3.0",
"chevrotain": "~11.1.1",
"chevrotain-allstar": "~0.3.1",
"vscode-languageserver": "~9.0.1",
"vscode-languageserver-textdocument": "~1.0.11",
"vscode-uri": "~3.0.8"
"vscode-uri": "~3.1.0"
},
"engines": {
"node": ">=16.0.0"
"node": ">=20.10.0",
"npm": ">=10.2.3"
}
},
"node_modules/layout-base": {
@@ -13091,14 +13074,14 @@
}
},
"node_modules/mermaid": {
"version": "11.12.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
"integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
"version": "11.12.3",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz",
"integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.1",
"@mermaid-js/parser": "^0.6.3",
"@mermaid-js/parser": "^1.0.0",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
"cytoscape-cose-bilkent": "^4.1.0",
@@ -13110,7 +13093,7 @@
"dompurify": "^3.2.5",
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"marked": "^16.2.1",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
@@ -18617,9 +18600,9 @@
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {

26
Dashboard/index.d.ts vendored
View File

@@ -2,3 +2,29 @@ declare module "*.png";
declare module "*.svg";
declare module "*.jpg";
declare module "*.gif";
declare module "react-syntax-highlighter/dist/esm/prism-light";
declare module "react-syntax-highlighter/dist/esm/styles/prism";
declare module "react-syntax-highlighter/dist/esm/languages/prism/javascript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/typescript";
declare module "react-syntax-highlighter/dist/esm/languages/prism/jsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/tsx";
declare module "react-syntax-highlighter/dist/esm/languages/prism/python";
declare module "react-syntax-highlighter/dist/esm/languages/prism/bash";
declare module "react-syntax-highlighter/dist/esm/languages/prism/json";
declare module "react-syntax-highlighter/dist/esm/languages/prism/yaml";
declare module "react-syntax-highlighter/dist/esm/languages/prism/sql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/go";
declare module "react-syntax-highlighter/dist/esm/languages/prism/java";
declare module "react-syntax-highlighter/dist/esm/languages/prism/css";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markup";
declare module "react-syntax-highlighter/dist/esm/languages/prism/markdown";
declare module "react-syntax-highlighter/dist/esm/languages/prism/docker";
declare module "react-syntax-highlighter/dist/esm/languages/prism/rust";
declare module "react-syntax-highlighter/dist/esm/languages/prism/c";
declare module "react-syntax-highlighter/dist/esm/languages/prism/cpp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/csharp";
declare module "react-syntax-highlighter/dist/esm/languages/prism/ruby";
declare module "react-syntax-highlighter/dist/esm/languages/prism/php";
declare module "react-syntax-highlighter/dist/esm/languages/prism/graphql";
declare module "react-syntax-highlighter/dist/esm/languages/prism/http";

View File

@@ -386,45 +386,45 @@
}
},
"../Common/node_modules/@aws-sdk/client-sso": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz",
"integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.990.0.tgz",
"integrity": "sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
"@aws-sdk/middleware-user-agent": "^3.972.5",
"@aws-sdk/middleware-user-agent": "^3.972.10",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/util-endpoints": "3.980.0",
"@aws-sdk/util-endpoints": "3.990.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.8",
"@smithy/config-resolver": "^4.4.6",
"@smithy/core": "^3.22.0",
"@smithy/core": "^3.23.0",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
"@smithy/middleware-endpoint": "^4.4.12",
"@smithy/middleware-retry": "^4.4.29",
"@smithy/middleware-endpoint": "^4.4.14",
"@smithy/middleware-retry": "^4.4.31",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/node-http-handler": "^4.4.8",
"@smithy/node-http-handler": "^4.4.10",
"@smithy/protocol-http": "^5.3.8",
"@smithy/smithy-client": "^4.11.1",
"@smithy/smithy-client": "^4.11.3",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
"@smithy/util-defaults-mode-browser": "^4.3.28",
"@smithy/util-defaults-mode-node": "^4.2.31",
"@smithy/util-defaults-mode-browser": "^4.3.30",
"@smithy/util-defaults-mode-node": "^4.2.33",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -436,9 +436,9 @@
}
},
"../Common/node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -453,20 +453,20 @@
}
},
"../Common/node_modules/@aws-sdk/core": {
"version": "3.973.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
"integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
"version": "3.973.10",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.10.tgz",
"integrity": "sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/xml-builder": "^3.972.2",
"@smithy/core": "^3.22.0",
"@aws-sdk/xml-builder": "^3.972.4",
"@smithy/core": "^3.23.0",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
"@smithy/smithy-client": "^4.11.1",
"@smithy/smithy-client": "^4.11.3",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-middleware": "^4.2.8",
@@ -478,13 +478,13 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz",
"integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.8.tgz",
"integrity": "sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/types": "^4.12.0",
@@ -495,21 +495,21 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-http": {
"version": "3.972.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz",
"integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==",
"version": "3.972.10",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.10.tgz",
"integrity": "sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/types": "^3.973.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/node-http-handler": "^4.4.8",
"@smithy/node-http-handler": "^4.4.10",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/smithy-client": "^4.11.1",
"@smithy/smithy-client": "^4.11.3",
"@smithy/types": "^4.12.0",
"@smithy/util-stream": "^4.5.10",
"@smithy/util-stream": "^4.5.12",
"tslib": "^2.6.2"
},
"engines": {
@@ -517,20 +517,20 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz",
"integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.8.tgz",
"integrity": "sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/credential-provider-env": "^3.972.3",
"@aws-sdk/credential-provider-http": "^3.972.5",
"@aws-sdk/credential-provider-login": "^3.972.3",
"@aws-sdk/credential-provider-process": "^3.972.3",
"@aws-sdk/credential-provider-sso": "^3.972.3",
"@aws-sdk/credential-provider-web-identity": "^3.972.3",
"@aws-sdk/nested-clients": "3.980.0",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/credential-provider-env": "^3.972.8",
"@aws-sdk/credential-provider-http": "^3.972.10",
"@aws-sdk/credential-provider-login": "^3.972.8",
"@aws-sdk/credential-provider-process": "^3.972.8",
"@aws-sdk/credential-provider-sso": "^3.972.8",
"@aws-sdk/credential-provider-web-identity": "^3.972.8",
"@aws-sdk/nested-clients": "3.990.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -543,14 +543,14 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-login": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
"integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.8.tgz",
"integrity": "sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/nested-clients": "3.980.0",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/nested-clients": "3.990.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
@@ -563,18 +563,18 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-node": {
"version": "3.972.4",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz",
"integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==",
"version": "3.972.9",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.9.tgz",
"integrity": "sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.3",
"@aws-sdk/credential-provider-http": "^3.972.5",
"@aws-sdk/credential-provider-ini": "^3.972.3",
"@aws-sdk/credential-provider-process": "^3.972.3",
"@aws-sdk/credential-provider-sso": "^3.972.3",
"@aws-sdk/credential-provider-web-identity": "^3.972.3",
"@aws-sdk/credential-provider-env": "^3.972.8",
"@aws-sdk/credential-provider-http": "^3.972.10",
"@aws-sdk/credential-provider-ini": "^3.972.8",
"@aws-sdk/credential-provider-process": "^3.972.8",
"@aws-sdk/credential-provider-sso": "^3.972.8",
"@aws-sdk/credential-provider-web-identity": "^3.972.8",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -587,13 +587,13 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-process": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz",
"integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.8.tgz",
"integrity": "sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -605,15 +605,15 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz",
"integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.8.tgz",
"integrity": "sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/client-sso": "3.980.0",
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/token-providers": "3.980.0",
"@aws-sdk/client-sso": "3.990.0",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/token-providers": "3.990.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -625,14 +625,14 @@
}
},
"../Common/node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz",
"integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.8.tgz",
"integrity": "sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/nested-clients": "3.980.0",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/nested-clients": "3.990.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -692,16 +692,16 @@
}
},
"../Common/node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.972.5",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
"integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
"version": "3.972.10",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.10.tgz",
"integrity": "sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/util-endpoints": "3.980.0",
"@smithy/core": "^3.22.0",
"@aws-sdk/util-endpoints": "3.990.0",
"@smithy/core": "^3.23.0",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -711,9 +711,9 @@
}
},
"../Common/node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -728,45 +728,45 @@
}
},
"../Common/node_modules/@aws-sdk/nested-clients": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
"integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz",
"integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
"@aws-sdk/middleware-user-agent": "^3.972.5",
"@aws-sdk/middleware-user-agent": "^3.972.10",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/util-endpoints": "3.980.0",
"@aws-sdk/util-endpoints": "3.990.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.8",
"@smithy/config-resolver": "^4.4.6",
"@smithy/core": "^3.22.0",
"@smithy/core": "^3.23.0",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
"@smithy/middleware-endpoint": "^4.4.12",
"@smithy/middleware-retry": "^4.4.29",
"@smithy/middleware-endpoint": "^4.4.14",
"@smithy/middleware-retry": "^4.4.31",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/node-http-handler": "^4.4.8",
"@smithy/node-http-handler": "^4.4.10",
"@smithy/protocol-http": "^5.3.8",
"@smithy/smithy-client": "^4.11.1",
"@smithy/smithy-client": "^4.11.3",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
"@smithy/util-defaults-mode-browser": "^4.3.28",
"@smithy/util-defaults-mode-node": "^4.2.31",
"@smithy/util-defaults-mode-browser": "^4.3.30",
"@smithy/util-defaults-mode-node": "^4.2.33",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -778,9 +778,9 @@
}
},
"../Common/node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
"integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz",
"integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -812,14 +812,14 @@
}
},
"../Common/node_modules/@aws-sdk/token-providers": {
"version": "3.980.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz",
"integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==",
"version": "3.990.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.990.0.tgz",
"integrity": "sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.5",
"@aws-sdk/nested-clients": "3.980.0",
"@aws-sdk/core": "^3.973.10",
"@aws-sdk/nested-clients": "3.990.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -888,13 +888,13 @@
}
},
"../Common/node_modules/@aws-sdk/util-user-agent-node": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
"integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
"version": "3.972.8",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.8.tgz",
"integrity": "sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-user-agent": "^3.972.5",
"@aws-sdk/middleware-user-agent": "^3.972.10",
"@aws-sdk/types": "^3.973.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/types": "^4.12.0",
@@ -913,9 +913,9 @@
}
},
"../Common/node_modules/@aws-sdk/xml-builder": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
"integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
"version": "3.972.4",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz",
"integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1687,54 +1687,42 @@
}
},
"../Common/node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz",
"integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "11.0.3",
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/gast": "11.1.1",
"@chevrotain/types": "11.1.1",
"lodash-es": "4.17.23"
}
},
"../Common/node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"../Common/node_modules/@chevrotain/gast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz",
"integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/types": "11.1.1",
"lodash-es": "4.17.23"
}
},
"../Common/node_modules/@chevrotain/gast/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"../Common/node_modules/@chevrotain/regexp-to-ast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz",
"integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==",
"license": "Apache-2.0"
},
"../Common/node_modules/@chevrotain/types": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz",
"integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==",
"license": "Apache-2.0"
},
"../Common/node_modules/@chevrotain/utils": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz",
"integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==",
"license": "Apache-2.0"
},
"../Common/node_modules/@clickhouse/client": {
@@ -3039,12 +3027,12 @@
"license": "MIT"
},
"../Common/node_modules/@mermaid-js/parser": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz",
"integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==",
"license": "MIT",
"dependencies": {
"langium": "3.3.1"
"langium": "^4.0.0"
}
},
"../Common/node_modules/@monaco-editor/loader": {
@@ -5084,9 +5072,9 @@
}
},
"../Common/node_modules/@smithy/core": {
"version": "3.22.1",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
"integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.2.tgz",
"integrity": "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -5096,7 +5084,7 @@
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-stream": "^4.5.11",
"@smithy/util-stream": "^4.5.12",
"@smithy/util-utf8": "^4.2.0",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
@@ -5198,13 +5186,13 @@
}
},
"../Common/node_modules/@smithy/middleware-endpoint": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz",
"integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==",
"version": "4.4.16",
"resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.16.tgz",
"integrity": "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.22.1",
"@smithy/core": "^3.23.2",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -5218,16 +5206,16 @@
}
},
"../Common/node_modules/@smithy/middleware-retry": {
"version": "4.4.30",
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz",
"integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==",
"version": "4.4.33",
"resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.33.tgz",
"integrity": "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/service-error-classification": "^4.2.8",
"@smithy/smithy-client": "^4.11.2",
"@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -5284,9 +5272,9 @@
}
},
"../Common/node_modules/@smithy/node-http-handler": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
"integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz",
"integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -5405,18 +5393,18 @@
}
},
"../Common/node_modules/@smithy/smithy-client": {
"version": "4.11.2",
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz",
"integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==",
"version": "4.11.5",
"resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.5.tgz",
"integrity": "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.22.1",
"@smithy/middleware-endpoint": "^4.4.13",
"@smithy/core": "^3.23.2",
"@smithy/middleware-endpoint": "^4.4.16",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"@smithy/util-stream": "^4.5.11",
"@smithy/util-stream": "^4.5.12",
"tslib": "^2.6.2"
},
"engines": {
@@ -5520,14 +5508,14 @@
}
},
"../Common/node_modules/@smithy/util-defaults-mode-browser": {
"version": "4.3.29",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz",
"integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==",
"version": "4.3.32",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.32.tgz",
"integrity": "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/property-provider": "^4.2.8",
"@smithy/smithy-client": "^4.11.2",
"@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -5536,9 +5524,9 @@
}
},
"../Common/node_modules/@smithy/util-defaults-mode-node": {
"version": "4.2.32",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz",
"integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==",
"version": "4.2.35",
"resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.35.tgz",
"integrity": "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -5546,7 +5534,7 @@
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/smithy-client": "^4.11.2",
"@smithy/smithy-client": "^4.11.5",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
@@ -5612,14 +5600,14 @@
}
},
"../Common/node_modules/@smithy/util-stream": {
"version": "4.5.11",
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz",
"integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==",
"version": "4.5.12",
"resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz",
"integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/node-http-handler": "^4.4.9",
"@smithy/node-http-handler": "^4.4.10",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-buffer-from": "^4.2.0",
@@ -8040,17 +8028,17 @@
}
},
"../Common/node_modules/chevrotain": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz",
"integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
"@chevrotain/regexp-to-ast": "11.0.3",
"@chevrotain/types": "11.0.3",
"@chevrotain/utils": "11.0.3",
"lodash-es": "4.17.21"
"@chevrotain/cst-dts-gen": "11.1.1",
"@chevrotain/gast": "11.1.1",
"@chevrotain/regexp-to-ast": "11.1.1",
"@chevrotain/types": "11.1.1",
"@chevrotain/utils": "11.1.1",
"lodash-es": "4.17.23"
}
},
"../Common/node_modules/chevrotain-allstar": {
@@ -8065,12 +8053,6 @@
"chevrotain": "^11.0.0"
}
},
"../Common/node_modules/chevrotain/node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"../Common/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -13296,19 +13278,20 @@
}
},
"../Common/node_modules/langium": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz",
"integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==",
"license": "MIT",
"dependencies": {
"chevrotain": "~11.0.3",
"chevrotain-allstar": "~0.3.0",
"chevrotain": "~11.1.1",
"chevrotain-allstar": "~0.3.1",
"vscode-languageserver": "~9.0.1",
"vscode-languageserver-textdocument": "~1.0.11",
"vscode-uri": "~3.0.8"
"vscode-uri": "~3.1.0"
},
"engines": {
"node": ">=16.0.0"
"node": ">=20.10.0",
"npm": ">=10.2.3"
}
},
"../Common/node_modules/layout-base": {
@@ -14106,14 +14089,14 @@
}
},
"../Common/node_modules/mermaid": {
"version": "11.12.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
"integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
"version": "11.12.3",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz",
"integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.1",
"@mermaid-js/parser": "^0.6.3",
"@mermaid-js/parser": "^1.0.0",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
"cytoscape-cose-bilkent": "^4.1.0",
@@ -14125,7 +14108,7 @@
"dompurify": "^3.2.5",
"katex": "^0.16.22",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"marked": "^16.2.1",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
@@ -19836,9 +19819,9 @@
"license": "MIT"
},
"../Common/node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"license": "MIT"
},
"../Common/node_modules/w3c-xmlserializer": {

View File

@@ -0,0 +1,101 @@
import React, { FunctionComponent, ReactElement, useState } from "react";
import MonitorStepDomainMonitor from "Common/Types/Monitor/MonitorStepDomainMonitor";
import Input, { InputType } from "Common/UI/Components/Input/Input";
import FieldLabelElement from "Common/UI/Components/Forms/Fields/FieldLabel";
import Button, { ButtonStyleType } from "Common/UI/Components/Button/Button";
export interface ComponentProps {
monitorStepDomainMonitor: MonitorStepDomainMonitor;
onChange: (value: MonitorStepDomainMonitor) => void;
}
const DomainMonitorStepForm: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [showAdvancedOptions, setShowAdvancedOptions] =
useState<boolean>(false);
return (
<div className="space-y-5">
<div>
<FieldLabelElement
title="Domain Name"
description="The domain name to monitor (e.g. example.com)"
required={true}
/>
<Input
initialValue={props.monitorStepDomainMonitor.domainName}
placeholder="example.com"
onChange={(value: string) => {
props.onChange({
...props.monitorStepDomainMonitor,
domainName: value,
});
}}
/>
</div>
{!showAdvancedOptions && (
<div className="mt-1 -ml-3">
<Button
title="Advanced: Timeout and Retries"
buttonStyle={ButtonStyleType.SECONDARY_LINK}
onClick={() => {
setShowAdvancedOptions(true);
}}
/>
</div>
)}
{showAdvancedOptions && (
<div className="space-y-4 border p-4 rounded-md bg-gray-50">
<h4 className="font-medium">Advanced Options</h4>
<div>
<FieldLabelElement
title="Timeout (ms)"
description="How long to wait for a WHOIS response before timing out"
required={false}
/>
<Input
initialValue={
props.monitorStepDomainMonitor.timeout?.toString() || "10000"
}
placeholder="10000"
type={InputType.NUMBER}
onChange={(value: string) => {
props.onChange({
...props.monitorStepDomainMonitor,
timeout: parseInt(value) || 10000,
});
}}
/>
</div>
<div>
<FieldLabelElement
title="Retries"
description="Number of times to retry on failure"
required={false}
/>
<Input
initialValue={
props.monitorStepDomainMonitor.retries?.toString() || "3"
}
placeholder="3"
type={InputType.NUMBER}
onChange={(value: string) => {
props.onChange({
...props.monitorStepDomainMonitor,
retries: parseInt(value) || 3,
});
}}
/>
</div>
</div>
)}
</div>
);
};
export default DomainMonitorStepForm;

View File

@@ -79,6 +79,10 @@ import DnsMonitorStepForm from "./DnsMonitor/DnsMonitorStepForm";
import MonitorStepDnsMonitor, {
MonitorStepDnsMonitorUtil,
} from "Common/Types/Monitor/MonitorStepDnsMonitor";
import DomainMonitorStepForm from "./DomainMonitor/DomainMonitorStepForm";
import MonitorStepDomainMonitor, {
MonitorStepDomainMonitorUtil,
} from "Common/Types/Monitor/MonitorStepDomainMonitor";
export interface ComponentProps {
monitorStatusDropdownOptions: Array<DropdownOption>;
@@ -812,6 +816,24 @@ return {
</Card>
)}
{props.monitorType === MonitorType.Domain && (
<Card
title="Domain Monitor Configuration"
description="Configure the domain registration monitoring settings"
>
<DomainMonitorStepForm
monitorStepDomainMonitor={
monitorStep.data?.domainMonitor ||
MonitorStepDomainMonitorUtil.getDefault()
}
onChange={(value: MonitorStepDomainMonitor) => {
monitorStep.setDomainMonitor(value);
props.onChange?.(MonitorStep.clone(monitorStep));
}}
/>
</Card>
)}
{/* Code Monitor Section */}
{isCodeMonitor && (
<Card

View File

@@ -310,6 +310,20 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
placeholder: "0",
},
];
} else if (props.monitorType === MonitorType.Domain) {
fields = [
{
key: "domainMonitor",
title: "Domain Name",
description: "The domain name being monitored via WHOIS.",
fieldType: FieldType.Element,
placeholder: "No data entered",
getElement: (item: MonitorStepType): ReactElement => {
const domainMonitor: any = item.domainMonitor;
return <p>{domainMonitor?.domainName || "-"}</p>;
},
},
];
} else if (props.monitorType === MonitorType.Logs) {
logFields = [];

View File

@@ -0,0 +1,170 @@
import OneUptimeDate from "Common/Types/Date";
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
import DomainMonitorResponse from "Common/Types/Monitor/DomainMonitor/DomainMonitorResponse";
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps {
probeMonitorResponse: ProbeMonitorResponse;
probeName?: string | undefined;
}
const DomainMonitorView: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const domainResponse: DomainMonitorResponse | undefined =
props.probeMonitorResponse?.domainResponse;
let responseTimeInMs: number = domainResponse?.responseTimeInMs || 0;
if (responseTimeInMs > 0) {
responseTimeInMs = Math.round(responseTimeInMs);
}
type FormatDateText = (dateStr: string | undefined) => string;
const formatDateText: FormatDateText = (
dateStr: string | undefined,
): string => {
if (!dateStr) {
return "-";
}
try {
const date: Date = new Date(dateStr);
return OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(date);
} catch {
return dateStr;
}
};
return (
<div className="space-y-5">
<div className="flex space-x-3">
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Probe"
value={props.probeName || "-"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Status"
value={props.probeMonitorResponse.isOnline ? "Online" : "Offline"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Response Time"
value={responseTimeInMs ? responseTimeInMs + " ms" : "-"}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Expires At"
value={formatDateText(domainResponse?.expiresDate)}
/>
<InfoCard
className="w-1/5 shadow-none border-2 border-gray-100"
title="Monitored At"
value={
props.probeMonitorResponse?.monitoredAt
? OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
props.probeMonitorResponse.monitoredAt,
)
: "-"
}
/>
</div>
<div className="flex space-x-3">
<InfoCard
className="w-1/3 shadow-none border-2 border-gray-100"
title="Registrar"
value={domainResponse?.registrar || "-"}
/>
<InfoCard
className="w-1/3 shadow-none border-2 border-gray-100"
title="Created"
value={formatDateText(domainResponse?.createdDate)}
/>
<InfoCard
className="w-1/3 shadow-none border-2 border-gray-100"
title="DNSSEC"
value={domainResponse?.dnssec || "-"}
/>
</div>
{props.probeMonitorResponse.failureCause && (
<div className="flex space-x-3">
<InfoCard
className="w-full shadow-none border-2 border-gray-100"
title="Error"
value={props.probeMonitorResponse.failureCause?.toString() || "-"}
/>
</div>
)}
{/* Name Servers Section */}
{domainResponse?.nameServers && domainResponse.nameServers.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">Name Servers</h3>
<div className="border rounded-md overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name Server
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{domainResponse.nameServers.map((ns: string, index: number) => {
return (
<tr key={index}>
<td className="px-4 py-2 text-sm text-gray-900 font-mono">
{ns}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
{/* Domain Status Section */}
{domainResponse?.domainStatus &&
domainResponse.domainStatus.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-700">
Domain Status Codes
</h3>
<div className="border rounded-md overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{domainResponse.domainStatus.map(
(status: string, index: number) => {
return (
<tr key={index}>
<td className="px-4 py-2 text-sm text-gray-900 font-mono">
{status}
</td>
</tr>
);
},
)}
</tbody>
</table>
</div>
</div>
)}
</div>
);
};
export default DomainMonitorView;

View File

@@ -396,7 +396,7 @@ const EvaluationLogList: FunctionComponent<ComponentProps> = (
};
return (
<div className="space-y-4">
<div className="mt-6 space-y-4">
<div className="text-base font-semibold text-gray-900">
{getSummaryTitle}
</div>

View File

@@ -8,6 +8,7 @@ import SyntheticMonitorView from "./SyntheticMonitorView";
import WebsiteMonitorSummaryView from "./WebsiteMonitorView";
import SnmpMonitorView from "./SnmpMonitorView";
import DnsMonitorView from "./DnsMonitorView";
import DomainMonitorView from "./DomainMonitorView";
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
import IncomingEmailMonitorRequest from "Common/Types/Monitor/IncomingEmailMonitor/IncomingEmailMonitorRequest";
import MonitorType, {
@@ -131,6 +132,15 @@ const SummaryInfo: FunctionComponent<ComponentProps> = (
);
}
if (props.monitorType === MonitorType.Domain) {
summaryComponent = (
<DomainMonitorView
probeMonitorResponse={probeMonitorResponse}
probeName={props.probeName}
/>
);
}
return (
<div key={key} className="space-y-6">
{summaryComponent}

View File

@@ -72,7 +72,7 @@ const MonitorCreate: FunctionComponent<
fieldType: FormFieldSchemaType.CardSelect,
required: true,
cardSelectOptions:
MonitorTypeUtil.monitorTypesAsCardSelectOptions(),
MonitorTypeUtil.monitorTypesAsCategorizedCardSelectOptions(),
},
{
field: {

View File

@@ -1,8 +1,11 @@
import MonitorTable from "../../Components/Monitor/MonitorTable";
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
const DisabledMonitors: FunctionComponent = (): ReactElement => {
const DisabledMonitors: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return (
<MonitorTable
query={{

View File

@@ -1,8 +1,11 @@
import MonitorTable from "../../Components/Monitor/MonitorTable";
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
const NotOperationalMonitors: FunctionComponent = (): ReactElement => {
const NotOperationalMonitors: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return (
<MonitorTable
query={{

View File

@@ -1,8 +1,11 @@
import MonitorTable from "../../Components/Monitor/MonitorTable";
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
const DisabledMonitors: FunctionComponent = (): ReactElement => {
const DisabledMonitors: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return (
<MonitorTable
query={{

View File

@@ -1,8 +1,11 @@
import MonitorTable from "../../Components/Monitor/MonitorTable";
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
const DisabledMonitors: FunctionComponent = (): ReactElement => {
const DisabledMonitors: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return (
<MonitorTable
query={{

View File

@@ -1,7 +1,10 @@
import IncomingCallNumber from "../../Components/NotificationMethods/IncomingCallNumber";
import PageComponentProps from "../PageComponentProps";
import React, { FunctionComponent, ReactElement } from "react";
const IncomingCallPhoneNumbers: FunctionComponent = (): ReactElement => {
const IncomingCallPhoneNumbers: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
return <IncomingCallNumber />;
};

View File

@@ -295,6 +295,18 @@ export default class CriteriaFilterUtil {
});
}
if (monitorType === MonitorType.Domain) {
options = options.filter((i: DropdownOption) => {
return (
i.value === CheckOn.DomainExpiresDaysIn ||
i.value === CheckOn.DomainRegistrar ||
i.value === CheckOn.DomainNameServer ||
i.value === CheckOn.DomainStatusCode ||
i.value === CheckOn.DomainIsExpired
);
});
}
return options;
}
@@ -570,6 +582,40 @@ export default class CriteriaFilterUtil {
});
}
if (checkOn === CheckOn.DomainExpiresDaysIn) {
options = options.filter((i: DropdownOption) => {
return (
i.value === FilterType.GreaterThan ||
i.value === FilterType.LessThan ||
i.value === FilterType.LessThanOrEqualTo ||
i.value === FilterType.GreaterThanOrEqualTo
);
});
}
if (
checkOn === CheckOn.DomainRegistrar ||
checkOn === CheckOn.DomainNameServer ||
checkOn === CheckOn.DomainStatusCode
) {
options = options.filter((i: DropdownOption) => {
return (
i.value === FilterType.Contains ||
i.value === FilterType.NotContains ||
i.value === FilterType.EqualTo ||
i.value === FilterType.NotEqualTo ||
i.value === FilterType.StartsWith ||
i.value === FilterType.EndsWith
);
});
}
if (checkOn === CheckOn.DomainIsExpired) {
options = options.filter((i: DropdownOption) => {
return i.value === FilterType.True || i.value === FilterType.False;
});
}
return options;
}
@@ -709,6 +755,22 @@ export default class CriteriaFilterUtil {
return "192.168.1.1";
}
if (checkOn === CheckOn.DomainExpiresDaysIn) {
return "30";
}
if (checkOn === CheckOn.DomainRegistrar) {
return "GoDaddy";
}
if (checkOn === CheckOn.DomainNameServer) {
return "ns1.example.com";
}
if (checkOn === CheckOn.DomainStatusCode) {
return "clientTransferProhibited";
}
return "";
}
}

View File

@@ -1,8 +1,12 @@
import {
import MonitorType, {
MonitorTypeCategory,
MonitorTypeHelper,
MonitorTypeProps,
} from "Common/Types/Monitor/MonitorType";
import { CardSelectOption } from "Common/UI/Components/CardSelect/CardSelect";
import {
CardSelectOption,
CardSelectOptionGroup,
} from "Common/UI/Components/CardSelect/CardSelect";
import { DropdownOption } from "Common/UI/Components/Dropdown/Dropdown";
export default class MonitorTypeUtil {
@@ -31,4 +35,43 @@ export default class MonitorTypeUtil {
};
});
}
public static monitorTypesAsCategorizedCardSelectOptions(): Array<CardSelectOptionGroup> {
const categories: Array<MonitorTypeCategory> =
MonitorTypeHelper.getMonitorTypeCategories();
const allProps: Array<MonitorTypeProps> =
MonitorTypeHelper.getAllMonitorTypeProps();
return categories.map(
(category: MonitorTypeCategory): CardSelectOptionGroup => {
return {
label: category.label,
options: category.monitorTypes
.map((monitorType: MonitorType): CardSelectOption | null => {
const typeProps: MonitorTypeProps | undefined = allProps.find(
(p: MonitorTypeProps) => {
return p.monitorType === monitorType;
},
);
if (!typeProps) {
return null;
}
return {
value: typeProps.monitorType,
title: typeProps.title,
description: typeProps.description,
icon: typeProps.icon,
};
})
.filter(
(option: CardSelectOption | null): option is CardSelectOption => {
return option !== null;
},
),
};
},
);
}
}

View File

@@ -0,0 +1,155 @@
# Authentication
The OneUptime CLI supports multiple ways to authenticate with your OneUptime instance. You can use named contexts, environment variables, or pass credentials directly as flags.
## Login
Authenticate with your OneUptime instance using an API key:
```bash
oneuptime login <api-key> <instance-url>
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `<api-key>` | Your OneUptime API key (e.g., `sk-your-api-key`) |
| `<instance-url>` | Your OneUptime instance URL (e.g., `https://oneuptime.com`) |
**Options:**
| Option | Description |
|--------|-------------|
| `--context-name <name>` | Name for this context (default: `"default"`) |
**Examples:**
```bash
# Login with default context
oneuptime login sk-abc123 https://oneuptime.com
# Login with a named context
oneuptime login sk-abc123 https://oneuptime.com --context-name production
# Set up multiple environments
oneuptime login sk-prod-key https://oneuptime.com --context-name production
oneuptime login sk-staging-key https://staging.oneuptime.com --context-name staging
```
## Contexts
Contexts allow you to save and switch between multiple OneUptime environments (e.g., production, staging, development).
### List Contexts
```bash
oneuptime context list
```
Displays all configured contexts. The current context is marked with `*`.
### Switch Context
```bash
oneuptime context use <name>
```
Switch to a different named context for all subsequent commands.
```bash
# Switch to staging
oneuptime context use staging
# Switch to production
oneuptime context use production
```
### View Current Context
```bash
oneuptime context current
```
Displays the currently active context, including the instance URL and a masked API key.
### Delete a Context
```bash
oneuptime context delete <name>
```
Remove a named context. If the deleted context is the current one, the CLI automatically switches to the first remaining context.
## Credential Resolution
Credentials are resolved in the following priority order:
1. **CLI flags** (`--api-key` and `--url`)
2. **Environment variables** (`ONEUPTIME_API_KEY` and `ONEUPTIME_URL`)
3. **Named context** (via `--context` flag)
4. **Current context** (from saved configuration)
You can mix sources -- for example, use an environment variable for the API key and a saved context for the URL.
### Using CLI Flags
```bash
oneuptime --api-key sk-abc123 --url https://oneuptime.com incident list
```
### Using Environment Variables
```bash
export ONEUPTIME_API_KEY=sk-abc123
export ONEUPTIME_URL=https://oneuptime.com
oneuptime incident list
```
### Using a Specific Context
```bash
oneuptime --context production incident list
```
## Verify Authentication
Check your current authentication status:
```bash
oneuptime whoami
```
This displays:
- Instance URL
- Masked API key
- Current context name (only shown if a saved context is active)
If not authenticated, the command shows a helpful message suggesting you run `oneuptime login`.
## Configuration File
Credentials are stored in `~/.oneuptime/config.json` with restricted permissions (`0600`).
```json
{
"currentContext": "production",
"contexts": {
"production": {
"name": "production",
"apiUrl": "https://oneuptime.com",
"apiKey": "sk-..."
},
"staging": {
"name": "staging",
"apiUrl": "https://staging.oneuptime.com",
"apiKey": "sk-..."
}
},
"defaults": {
"output": "table",
"limit": 10
}
}
```

View File

@@ -0,0 +1,234 @@
# Command Reference
Complete reference for all OneUptime CLI commands.
## Authentication Commands
### `oneuptime login`
Authenticate with a OneUptime instance.
```bash
oneuptime login <api-key> <instance-url> [--context-name <name>]
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<api-key>` | argument | Yes | API key for authentication |
| `<instance-url>` | argument | Yes | OneUptime instance URL |
| `--context-name` | option | No | Context name (default: `"default"`) |
---
### `oneuptime context list`
List all saved contexts.
```bash
oneuptime context list
```
---
### `oneuptime context use`
Switch to a named context.
```bash
oneuptime context use <name>
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<name>` | argument | Yes | Context name to activate |
---
### `oneuptime context current`
Display the active context with masked API key.
```bash
oneuptime context current
```
---
### `oneuptime context delete`
Remove a saved context.
```bash
oneuptime context delete <name>
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<name>` | argument | Yes | Context name to delete |
---
## Resource Commands
All resource commands follow the same pattern. Replace `<resource>` with any supported resource name (e.g., `incident`, `monitor`, `alert`, `status-page`).
### `oneuptime <resource> list`
List resources with filtering and pagination.
```bash
oneuptime <resource> list [options]
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--query <json>` | string | None | Filter criteria as JSON |
| `--limit <n>` | number | `10` | Maximum results |
| `--skip <n>` | number | `0` | Results to skip |
| `--sort <json>` | string | None | Sort order as JSON |
| `-o, --output` | string | `table` | Output format |
---
### `oneuptime <resource> get`
Get a single resource by ID.
```bash
oneuptime <resource> get <id> [-o <format>]
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<id>` | argument | Yes | Resource ID (UUID) |
| `-o, --output` | option | No | Output format |
---
### `oneuptime <resource> create`
Create a new resource.
```bash
oneuptime <resource> create [--data <json> | --file <path>] [-o <format>]
```
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `--data <json>` | string | One of `--data` or `--file` | Resource data as JSON |
| `--file <path>` | string | One of `--data` or `--file` | Path to JSON file |
| `-o, --output` | string | No | Output format |
---
### `oneuptime <resource> update`
Update an existing resource.
```bash
oneuptime <resource> update <id> --data <json> [-o <format>]
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<id>` | argument | Yes | Resource ID |
| `--data <json>` | option | Yes | Fields to update as JSON |
| `-o, --output` | option | No | Output format |
---
### `oneuptime <resource> delete`
Delete a resource.
```bash
oneuptime <resource> delete <id> [--force]
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `<id>` | argument | Yes | Resource ID |
| `--force` | option | No | Skip confirmation prompt |
---
### `oneuptime <resource> count`
Count resources matching a filter.
```bash
oneuptime <resource> count [--query <json>]
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--query <json>` | string | None | Filter criteria as JSON |
---
## Utility Commands
### `oneuptime version`
Display the CLI version.
```bash
oneuptime version
```
---
### `oneuptime whoami`
Show current authentication details.
```bash
oneuptime whoami
```
Displays the instance URL and masked API key. If a saved context is active, the context name is also shown.
---
### `oneuptime resources`
List all available resource types.
```bash
oneuptime resources [--type <type>]
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--type <type>` | string | None | Filter by `database` or `analytics` |
---
## Global Options
These flags are available on all commands:
| Option | Description |
|--------|-------------|
| `--api-key <key>` | Override API key |
| `--url <url>` | Override instance URL |
| `--context <name>` | Use a specific context |
| `-o, --output <format>` | Output format: `json`, `table`, `wide` |
| `--no-color` | Disable colored output |
| `--help` | Show help |
| `--version` | Show version |
## API Routes
For reference, the CLI maps commands to these API endpoints:
| Command | Method | Endpoint |
|---------|--------|----------|
| `list` | POST | `/api/<resource>/get-list` |
| `get` | POST | `/api/<resource>/<id>/get-item` |
| `create` | POST | `/api/<resource>` |
| `update` | PUT | `/api/<resource>/<id>/` |
| `delete` | DELETE | `/api/<resource>/<id>/` |
| `count` | POST | `/api/<resource>/count` |
All requests include the `APIKey` header for authentication.

68
Docs/Content/cli/index.md Normal file
View File

@@ -0,0 +1,68 @@
# OneUptime CLI
The OneUptime CLI is a command-line interface for managing your OneUptime resources directly from the terminal. It supports full CRUD operations on monitors, incidents, alerts, status pages, and more.
## Features
- **Multi-environment support** with named contexts for production, staging, and development
- **Auto-discovery** of available resources from your OneUptime instance
- **Flexible authentication** via CLI flags, environment variables, or saved contexts
- **Smart output formatting** with JSON, table, and wide display modes
- **Scriptable** for CI/CD pipelines and automation workflows
## Installation
```bash
npm install -g @oneuptime/cli
```
## Quick Start
```bash
# Authenticate with your OneUptime instance
oneuptime login <your-api-key> https://oneuptime.com
# List your monitors
oneuptime monitor list
# View a specific incident
oneuptime incident get <incident-id>
# See all available resources
oneuptime resources
```
## Documentation
| Guide | Description |
|-------|-------------|
| [Authentication](./authentication.md) | Login, contexts, and credential management |
| [Resource Operations](./resource-operations.md) | CRUD operations on monitors, incidents, alerts, and more |
| [Output Formats](./output-formats.md) | JSON, table, and wide output modes |
| [Scripting and CI/CD](./scripting.md) | Automation, environment variables, and pipeline usage |
| [Command Reference](./command-reference.md) | Complete reference for all commands and options |
## Global Options
These flags can be used with any command:
| Flag | Description |
|------|-------------|
| `--api-key <key>` | Override API key for this command |
| `--url <url>` | Override instance URL for this command |
| `--context <name>` | Use a specific named context |
| `-o, --output <format>` | Output format: `json`, `table`, `wide` |
| `--no-color` | Disable colored output |
| `--help` | Show command help |
| `--version` | Show CLI version |
## Getting Help
```bash
# General help
oneuptime --help
# Help for a specific command
oneuptime monitor --help
oneuptime monitor list --help
```

View File

@@ -0,0 +1,80 @@
# Output Formats
The OneUptime CLI supports three output formats: **table**, **JSON**, and **wide**. You can set the format with the `-o` or `--output` flag on any command.
## Table (Default)
The default format when running in an interactive terminal. Displays results as an ASCII table with intelligently selected columns.
```bash
oneuptime incident list
```
```
┌──────────────────┬───────────────────────┬─────────────────────┬─────────────────────┐
│ _id │ title │ createdAt │ updatedAt │
├──────────────────┼───────────────────────┼─────────────────────┼─────────────────────┤
│ abc-123 │ API Outage │ 2025-01-15T10:30:00 │ 2025-01-15T12:00:00 │
│ def-456 │ Database Slowdown │ 2025-01-14T08:15:00 │ 2025-01-14T09:30:00 │
└──────────────────┴───────────────────────┴─────────────────────┴─────────────────────┘
```
Table format behavior:
- Selects up to 6 columns, prioritizing: `_id`, `name`, `title`, `createdAt`, `updatedAt`
- Truncates values longer than 60 characters with `...`
- Uses color-coded headers (disable with `--no-color`)
## JSON
Raw JSON output, pretty-printed with 2-space indentation. This is the best format for scripting and piping to other tools.
```bash
oneuptime incident list -o json
```
```json
[
{
"_id": "abc-123",
"title": "API Outage",
"currentIncidentStateId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2025-01-15T10:30:00Z"
}
]
```
JSON format is automatically used when the output is piped to another command (non-TTY mode):
```bash
# JSON is used automatically when piping
oneuptime incident list | jq '.[].title'
```
## Wide
Displays all columns without truncation. Useful for detailed inspection but may produce very wide output.
```bash
oneuptime incident list -o wide
```
## Disabling Color
Color output can be disabled in several ways:
```bash
# Using the --no-color flag
oneuptime --no-color incident list
# Using the NO_COLOR environment variable
NO_COLOR=1 oneuptime incident list
```
## Special Output Cases
| Scenario | Output |
|----------|--------|
| Empty result set | `"No results found."` |
| No data returned | `"No data returned."` |
| Single object (e.g., `get`) | Key-value table format |
| `count` command | Plain numeric value |

View File

@@ -0,0 +1,230 @@
# Resource Operations
The OneUptime CLI provides full CRUD (Create, Read, Update, Delete) operations for all supported resources. Resources are auto-discovered from your OneUptime instance.
## Available Resources
Run the following command to see all available resource types:
```bash
oneuptime resources
```
You can filter by type:
```bash
# Show only database resources
oneuptime resources --type database
# Show only analytics resources
oneuptime resources --type analytics
```
Common resources include:
| Resource | Command |
|----------|---------|
| Incident | `oneuptime incident` |
| Alert | `oneuptime alert` |
| Monitor | `oneuptime monitor` |
| Monitor Status | `oneuptime monitor-status` |
| Incident State | `oneuptime incident-state` |
| Status Page | `oneuptime status-page` |
| On-Call Policy | `oneuptime on-call-policy` |
| Team | `oneuptime team` |
| Scheduled Maintenance Event | `oneuptime scheduled-maintenance-event` |
## List Resources
Retrieve a list of resources with optional filtering, pagination, and sorting.
```bash
oneuptime <resource> list [options]
```
**Options:**
| Option | Description | Default |
|--------|-------------|---------|
| `--query <json>` | Filter criteria as JSON | None |
| `--limit <n>` | Maximum number of results | `10` |
| `--skip <n>` | Number of results to skip | `0` |
| `--sort <json>` | Sort order as JSON | None |
| `-o, --output <format>` | Output format | `table` |
**Examples:**
```bash
# List the 10 most recent incidents
oneuptime incident list
# Filter incidents by state ID
oneuptime incident list --query '{"currentIncidentStateId":"<state-id>"}'
# List with pagination
oneuptime incident list --limit 20 --skip 40
# Sort by creation date (descending)
oneuptime incident list --sort '{"createdAt":-1}'
# Output as JSON
oneuptime incident list -o json
```
## Get a Resource
Retrieve a single resource by its ID.
```bash
oneuptime <resource> get <id>
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `<id>` | The resource ID (UUID) |
**Examples:**
```bash
# Get a specific incident
oneuptime incident get 550e8400-e29b-41d4-a716-446655440000
# Get a monitor as JSON
oneuptime monitor get abc-123 -o json
```
## Create a Resource
Create a new resource from inline JSON or a file.
```bash
oneuptime <resource> create [options]
```
**Options:**
| Option | Description |
|--------|-------------|
| `--data <json>` | Resource data as a JSON object |
| `--file <path>` | Path to a JSON file containing resource data |
| `-o, --output <format>` | Output format |
You must provide either `--data` or `--file`.
**Examples:**
```bash
# Create an incident with inline JSON
oneuptime incident create --data '{"title":"API Outage","currentIncidentStateId":"<state-id>","incidentSeverityId":"<severity-id>","declaredAt":"2025-01-15T10:30:00Z"}'
# Create from a JSON file
oneuptime incident create --file incident.json
# Create and output as JSON to capture the ID
oneuptime monitor create --data '{"name":"API Health Check"}' -o json
```
## Update a Resource
Update an existing resource by ID.
```bash
oneuptime <resource> update <id> [options]
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `<id>` | The resource ID |
**Options:**
| Option | Description |
|--------|-------------|
| `--data <json>` | Fields to update as JSON (required) |
| `-o, --output <format>` | Output format |
**Examples:**
```bash
# Change incident state (e.g., to resolved)
oneuptime incident update abc-123 --data '{"currentIncidentStateId":"<resolved-state-id>"}'
# Rename a monitor
oneuptime monitor update abc-123 --data '{"name":"Updated Monitor Name"}'
```
## Delete a Resource
Delete a resource by ID.
```bash
oneuptime <resource> delete <id> [--force]
```
**Arguments:**
| Argument | Description |
|----------|-------------|
| `<id>` | The resource ID |
**Options:**
| Option | Description |
|--------|-------------|
| `--force` | Skip confirmation prompt |
**Examples:**
```bash
oneuptime incident delete abc-123
oneuptime monitor delete 550e8400-e29b-41d4-a716-446655440000
# Skip confirmation
oneuptime monitor delete 550e8400-e29b-41d4-a716-446655440000 --force
```
## Count Resources
Count resources matching optional filter criteria.
```bash
oneuptime <resource> count [options]
```
**Options:**
| Option | Description |
|--------|-------------|
| `--query <json>` | Filter criteria as JSON |
**Examples:**
```bash
# Count all incidents
oneuptime incident count
# Count incidents by state
oneuptime incident count --query '{"currentIncidentStateId":"<state-id>"}'
# Count monitors
oneuptime monitor count
```
## Analytics Resources
Analytics resources support a limited set of operations compared to database resources:
| Operation | Supported |
|-----------|-----------|
| `list` | Yes |
| `create` | Yes |
| `count` | Yes |
| `get` | No |
| `update` | No |
| `delete` | No |
Use `oneuptime resources --type analytics` to see which analytics resources are available on your instance.

View File

@@ -0,0 +1,152 @@
# Scripting and CI/CD
The OneUptime CLI is designed for automation. It supports environment-variable-based authentication, JSON output for programmatic parsing, and appropriate exit codes for pipeline integration.
## Environment Variables
Set these environment variables to authenticate without saved contexts:
```bash
export ONEUPTIME_API_KEY=sk-your-api-key
export ONEUPTIME_URL=https://oneuptime.com
```
These take precedence over saved contexts but are overridden by CLI flags.
## Exit Codes
| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | General error |
| `2` | Authentication error (missing or invalid credentials) |
| `3` | Not found (404) |
Use exit codes in scripts to handle errors:
```bash
if ! oneuptime monitor list > /dev/null 2>&1; then
echo "Failed to list monitors"
exit 1
fi
```
## JSON Processing with jq
Use `-o json` to produce machine-readable output:
```bash
# Extract all incident titles
oneuptime incident list -o json | jq '.[].title'
# Get the ID of a newly created monitor
NEW_ID=$(oneuptime monitor create --data '{"name":"API Health"}' -o json | jq -r '._id')
echo "Created monitor: $NEW_ID"
# Count incidents by severity
oneuptime incident count --query '{"incidentSeverityId":"<severity-id>"}'
```
## Creating Resources from Files
Use `--file` to create resources from JSON files, useful for version-controlled infrastructure:
```bash
# monitor.json
# {
# "name": "API Health Check",
# "projectId": "your-project-id"
# }
oneuptime monitor create --file monitor.json
```
## Batch Operations
Process multiple resources in a loop:
```bash
# Create multiple monitors from a JSON array file
cat monitors.json | jq -r '.[] | @json' | while read monitor; do
oneuptime monitor create --data "$monitor"
done
```
## CI/CD Pipeline Examples
### GitHub Actions
```yaml
name: Check Active Incidents
on:
schedule:
- cron: '*/5 * * * *'
jobs:
health-check:
runs-on: ubuntu-latest
steps:
- name: Install OneUptime CLI
run: npm install -g @oneuptime/cli
- name: Check for active incidents
env:
ONEUPTIME_API_KEY: ${{ secrets.ONEUPTIME_API_KEY }}
ONEUPTIME_URL: https://oneuptime.com
run: |
INCIDENT_COUNT=$(oneuptime incident count)
if [ "$INCIDENT_COUNT" -gt 0 ]; then
echo "WARNING: $INCIDENT_COUNT incidents found"
exit 1
fi
```
### Generic CI/CD Script
```bash
#!/bin/bash
set -e
export ONEUPTIME_API_KEY="$CI_ONEUPTIME_API_KEY"
export ONEUPTIME_URL="$CI_ONEUPTIME_URL"
# Create a deployment incident and capture the ID
# Note: currentIncidentStateId and incidentSeverityId must reference existing state/severity IDs in your project
INCIDENT_ID=$(oneuptime incident create --data '{
"title": "Deployment Started",
"currentIncidentStateId": "'"$INVESTIGATING_STATE_ID"'",
"incidentSeverityId": "'"$SEVERITY_ID"'",
"declaredAt": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
}' -o json | jq -r '._id')
# Run deployment steps here...
# Resolve the incident after successful deployment
oneuptime incident update "$INCIDENT_ID" --data '{"currentIncidentStateId":"'"$RESOLVED_STATE_ID"'"}'
```
### Docker
```dockerfile
FROM node:20-slim
RUN npm install -g @oneuptime/cli
ENV ONEUPTIME_API_KEY=""
ENV ONEUPTIME_URL=""
ENTRYPOINT ["oneuptime"]
```
```bash
docker run --rm \
-e ONEUPTIME_API_KEY=sk-abc123 \
-e ONEUPTIME_URL=https://oneuptime.com \
oneuptime-cli incident list
```
## Using a Specific Context in Scripts
If you have multiple contexts saved, target a specific one:
```bash
oneuptime --context production incident list
oneuptime --context staging monitor count
```

View File

@@ -130,6 +130,35 @@ const DocsNav: NavGroup[] = [
},
],
},
{
title: "CLI",
links: [
{
title: "Overview",
url: "/docs/cli/index",
},
{
title: "Authentication",
url: "/docs/cli/authentication",
},
{
title: "Resource Operations",
url: "/docs/cli/resource-operations",
},
{
title: "Output Formats",
url: "/docs/cli/output-formats",
},
{
title: "Scripting & CI/CD",
url: "/docs/cli/scripting",
},
{
title: "Command Reference",
url: "/docs/cli/command-reference",
},
],
},
{
title: "Monitor",
links: [

View File

@@ -203,6 +203,12 @@ Usage:
- name: VAPID_PRIVATE_KEY
value: {{ $.Values.vapid.privateKey }}
- name: EXPO_ACCESS_TOKEN
value: {{ default "" $.Values.expo.accessToken | quote }}
- name: PUSH_NOTIFICATION_RELAY_URL
value: {{ default "https://oneuptime.com/api/notification/push-relay/send" $.Values.pushNotification.relayUrl | quote }}
- name: SLACK_APP_CLIENT_SECRET
value: {{ $.Values.slackApp.clientSecret }}

View File

@@ -73,12 +73,23 @@ spec:
{{- end }}
imagePullPolicy: {{ $.Values.image.pullPolicy }}
env:
{{- include "oneuptime.env.common" . | nindent 12 }}
{{- include "oneuptime.env.oneuptimeSecret" . | nindent 12 }}
- name: PORT
value: {{ $.Values.isolatedVM.ports.http | quote }}
- name: LOG_LEVEL
value: {{ $.Values.logLevel }}
- name: NODE_ENV
value: {{ $.Values.nodeEnvironment }}
- name: DISABLE_TELEMETRY
value: {{ $.Values.isolatedVM.disableTelemetryCollection | quote }}
{{- if $.Values.openTelemetryExporter.endpoint }}
- name: OPENTELEMETRY_EXPORTER_OTLP_ENDPOINT
value: {{ $.Values.openTelemetryExporter.endpoint }}
{{- end }}
{{- if $.Values.openTelemetryExporter.headers }}
- name: OPENTELEMETRY_EXPORTER_OTLP_HEADERS
value: {{ $.Values.openTelemetryExporter.headers }}
{{- end }}
ports:
- containerPort: {{ $.Values.isolatedVM.ports.http }}

View File

@@ -131,7 +131,7 @@ spec:
- name: NO_PROXY
value: {{ $val.proxy.noProxy | squote }}
{{- end }}
{{- include "oneuptime.env.runtime" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
{{- include "oneuptime.env.oneuptimeSecret" (dict "Values" $.Values "Release" $.Release) | nindent 12 }}
ports:
- containerPort: {{ if and $val.ports $val.ports.http }}{{ $val.ports.http }}{{ else }}3874{{ end }}
protocol: TCP

View File

@@ -727,6 +727,24 @@
},
"additionalProperties": false
},
"expo": {
"type": "object",
"properties": {
"accessToken": {
"type": ["string", "null"]
}
},
"additionalProperties": false
},
"pushNotification": {
"type": "object",
"properties": {
"relayUrl": {
"type": ["string", "null"]
}
},
"additionalProperties": false
},
"incidents": {
"type": "object",
"properties": {

View File

@@ -287,6 +287,15 @@ vapid:
privateKey:
subject: mailto:support@oneuptime.com
# Expo access token for sending mobile push notifications directly via Expo SDK.
# If not set, notifications are relayed through the push notification relay URL.
expo:
accessToken:
# Push notification relay URL for self-hosted instances without Expo credentials
pushNotification:
relayUrl: https://oneuptime.com/api/notification/push-relay/send
incidents:
disableAutomaticCreation: false

View File

@@ -1257,6 +1257,31 @@ const HomeFeatureSet: FeatureSet = {
res.redirect("/product/ai-agent");
});
app.get(
"/tool/mcp-server",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/tool/mcp-server",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/mcp-server`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
app.get("/tool/cli", (_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/tool/cli",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/cli`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
});
app.get(
"/enterprise/overview",
(_req: ExpressRequest, res: ExpressResponse) => {

View File

@@ -305,6 +305,72 @@ export const PageSEOConfig: Record<string, PageSEOData> = {
},
},
"/tool/mcp-server": {
title: "MCP Server | Model Context Protocol for AI Agents | OneUptime",
description:
"Connect AI agents and LLMs to your OneUptime observability data via Model Context Protocol (MCP). Query incidents, monitors, logs, metrics, and traces directly from your AI tools.",
canonicalPath: "/tool/mcp-server",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "MCP Server", url: "/tool/mcp-server" },
],
softwareApplication: {
name: "OneUptime MCP Server",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Model Context Protocol server that connects AI agents and LLMs to OneUptime observability data for querying incidents, monitors, logs, metrics, and traces.",
features: [
"Incident querying and management",
"Monitor status and health checks",
"Log search and filtering",
"Metrics time series retrieval",
"Distributed trace analysis",
"Compatible with Claude, Cursor, Windsurf",
"API key authentication",
"Fine-grained permissions",
"Real-time data access",
"Open protocol standard",
],
},
},
"/tool/cli": {
title: "CLI | Command Line Interface for Observability | OneUptime",
description:
"OneUptime CLI lets you manage monitors, incidents, status pages, and observability data from your terminal. Deploy, configure, and automate your monitoring infrastructure with simple commands.",
canonicalPath: "/tool/cli",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "CLI", url: "/tool/cli" },
],
softwareApplication: {
name: "OneUptime CLI",
applicationCategory: "DeveloperApplication",
operatingSystem: "macOS, Linux, Windows",
description:
"Command line interface for managing OneUptime monitors, incidents, status pages, and observability data from your terminal.",
features: [
"Monitor creation and management",
"Incident response from terminal",
"Status page management",
"Real-time log tailing",
"CI/CD pipeline integration",
"Scriptable JSON output",
"YAML configuration support",
"Bulk operations",
"npm, Homebrew, and Docker install",
"API key and browser authentication",
],
},
},
"/product/metrics": {
title: "Metrics | Application & Infrastructure Metrics | OneUptime",
description:

547
Home/Views/cli.ejs Normal file
View File

@@ -0,0 +1,547 @@
<!DOCTYPE html>
<html lang="en" id="home">
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<head>
<title>OneUptime | CLI - Command Line Interface for Observability</title>
<meta name="description"
content="OneUptime CLI lets you manage monitors, incidents, status pages, and observability data from your terminal. Deploy, configure, and automate your monitoring infrastructure with simple commands.">
<%- include('head', {
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
}) -%>
</head>
<body>
<%- include('nav') -%>
<main id="main-content">
<!-- Hero Section -->
<div class="relative isolate overflow-hidden bg-white" id="cli-hero-section">
<!-- Subtle grid pattern background -->
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
<!-- Grid glow effect that follows cursor -->
<div id="cli-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(16, 185, 129, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(16, 185, 129, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
<div class="py-20 sm:py-28 lg:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-3xl text-center">
<!-- Minimal badge -->
<p class="text-sm font-medium text-emerald-600 mb-4">Command Line Interface</p>
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
Observability from your terminal
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
Manage monitors, incidents, status pages, and your entire observability stack from the command line. Automate workflows, integrate with CI/CD, and stay in your terminal.
</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<a href="/accounts/register"
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
Get started
</a>
<a href="/docs/cli" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
Read docs <span aria-hidden="true">&rarr;</span>
</a>
</div>
<!-- Subtle feature list -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
<span>Monitor management</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Incident response</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>CI/CD integration</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Scriptable</span>
</div>
</div>
<!-- Terminal mockup -->
<div class="mt-16 sm:mt-20">
<div class="relative mx-auto max-w-3xl">
<div class="rounded-xl bg-gray-900 p-1.5 ring-1 ring-gray-900/10 shadow-2xl">
<!-- Terminal header -->
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
</div>
<!-- Terminal content -->
<div class="p-6 font-mono text-sm space-y-3">
<div>
<span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor list</span>
</div>
<div class="text-gray-400 pl-2 space-y-1">
<div class="flex items-center gap-3">
<span class="text-emerald-400">&#9679;</span>
<span class="text-gray-300 w-40">API Gateway</span>
<span class="text-emerald-400">Up</span>
<span class="text-gray-500">99.98%</span>
</div>
<div class="flex items-center gap-3">
<span class="text-emerald-400">&#9679;</span>
<span class="text-gray-300 w-40">Web App</span>
<span class="text-emerald-400">Up</span>
<span class="text-gray-500">99.95%</span>
</div>
<div class="flex items-center gap-3">
<span class="text-red-400">&#9679;</span>
<span class="text-gray-300 w-40">Payment Service</span>
<span class="text-red-400">Down</span>
<span class="text-gray-500">98.50%</span>
</div>
<div class="flex items-center gap-3">
<span class="text-emerald-400">&#9679;</span>
<span class="text-gray-300 w-40">Database</span>
<span class="text-emerald-400">Up</span>
<span class="text-gray-500">99.99%</span>
</div>
</div>
<div class="mt-4">
<span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident create --title "Payment Service Down" --severity critical</span>
</div>
<div class="text-emerald-400 pl-2">
Incident INC-2847 created successfully. On-call team notified.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('logo-roll') -%>
<!-- How It Works -->
<div class="relative bg-gray-50 py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<p class="text-sm font-medium text-emerald-600 uppercase tracking-wide mb-3">Get Started</p>
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
Up and running in seconds
</h2>
<p class="mt-4 text-lg text-gray-600">
Install, authenticate, and start managing your infrastructure from the terminal.
</p>
</div>
<div class="mx-auto max-w-5xl">
<!-- Connecting line for desktop -->
<div class="hidden lg:block relative">
<div class="absolute top-8 left-[calc(16.67%+24px)] right-[calc(16.67%+24px)] h-px bg-emerald-200"></div>
</div>
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3">
<!-- Step 1 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">1</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Install</h3>
<p class="text-sm text-gray-600 leading-relaxed">Install the CLI via npm, Homebrew, or download the binary directly.</p>
</div>
<!-- Step 2 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">2</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Authenticate</h3>
<p class="text-sm text-gray-600 leading-relaxed">Login with your API key or authenticate via the browser.</p>
</div>
<!-- Step 3 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-emerald-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-emerald-600 ring-2 ring-emerald-600">3</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Run</h3>
<p class="text-sm text-gray-600 leading-relaxed">Start managing your monitors, incidents, and observability data from the terminal.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Why CLI Section -->
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-2xl text-center mb-20">
<div class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-4 py-1.5 text-sm font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 mb-6">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
</svg>
Why CLI
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
Power users love the terminal
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Everything you can do in the dashboard, now available as commands you can script, pipe, and automate.
</p>
</div>
<!-- Feature Blocks -->
<div class="space-y-24 lg:space-y-32">
<!-- Feature 1: Monitor Management -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
<svg class="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span class="text-sm font-semibold text-emerald-600 uppercase tracking-wide">Monitor Management</span>
</div>
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Create and Manage Monitors</h3>
<p class="text-lg text-gray-600 mb-6">
Create, update, and manage monitors directly from the command line. List monitor statuses, view uptime history, and configure alerts without opening a browser.
</p>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Create monitors from YAML configs
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
View real-time status and uptime
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Bulk operations for large setups
</li>
</ul>
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
<div class="mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Terminal Mockup -->
<div class="relative bg-gray-900 rounded-xl shadow-lg overflow-hidden max-w-md mx-auto ring-1 ring-gray-700/50">
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
</div>
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
</div>
<div class="p-4 font-mono text-xs space-y-2">
<div><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor create \</span></div>
<div class="text-gray-300 pl-4">--name "API Health Check" \</div>
<div class="text-gray-300 pl-4">--type http \</div>
<div class="text-gray-300 pl-4">--url https://api.example.com/health \</div>
<div class="text-gray-300 pl-4">--interval 30s</div>
<div class="mt-2 text-emerald-400">Monitor created: mon_8x7k2m9p</div>
<div class="text-gray-500">Type: HTTP | Interval: 30s | Status: Active</div>
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime monitor status mon_8x7k2m9p</span></div>
<div class="mt-1 text-emerald-400">&#9679; Up - 200 OK (45ms)</div>
<div class="text-gray-500">Last checked: 12s ago</div>
</div>
</div>
</div>
</div>
</div>
<!-- Feature 2: Incident Response -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative order-2 lg:order-1 mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Terminal Mockup -->
<div class="relative bg-gray-900 rounded-xl shadow-lg overflow-hidden max-w-md mx-auto ring-1 ring-gray-700/50">
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
</div>
<span class="text-gray-400 text-xs font-mono ml-2">Terminal</span>
</div>
<div class="p-4 font-mono text-xs space-y-2">
<div><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident list --state active</span></div>
<div class="mt-1 space-y-1.5">
<div class="flex items-center gap-2">
<span class="text-red-400">&#9679;</span>
<span class="text-gray-300">INC-2847</span>
<span class="text-red-400 text-[10px] px-1.5 py-0.5 bg-red-900/30 rounded">Critical</span>
<span class="text-gray-400">Payment Service Down</span>
</div>
<div class="flex items-center gap-2">
<span class="text-amber-400">&#9679;</span>
<span class="text-gray-300">INC-2846</span>
<span class="text-amber-400 text-[10px] px-1.5 py-0.5 bg-amber-900/30 rounded">Warning</span>
<span class="text-gray-400">High latency on DB</span>
</div>
</div>
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident acknowledge INC-2847</span></div>
<div class="text-emerald-400 mt-1">Incident INC-2847 acknowledged.</div>
<div class="mt-3"><span class="text-emerald-400">$</span> <span class="text-gray-300">oneuptime incident resolve INC-2847 \</span></div>
<div class="text-gray-300 pl-4">--note "Restarted payment service pod"</div>
<div class="text-emerald-400 mt-1">Incident INC-2847 resolved.</div>
</div>
</div>
</div>
</div>
<div class="relative order-1 lg:order-2">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
<svg class="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
</div>
<span class="text-sm font-semibold text-emerald-600 uppercase tracking-wide">Incident Response</span>
</div>
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Respond to Incidents Faster</h3>
<p class="text-lg text-gray-600 mb-6">
Acknowledge, investigate, and resolve incidents without leaving your terminal. Add notes, change severity, and notify your team all from the command line.
</p>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Acknowledge and resolve incidents
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Add investigation notes
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-emerald-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Trigger on-call escalations
</li>
</ul>
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Features Grid Section -->
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<div class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-4 py-1.5 text-sm font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 mb-6">
<svg class="h-4 w-4 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
</svg>
<span class="text-sm font-medium text-emerald-700">Capabilities</span>
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Everything you need in the terminal
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
A complete command-line toolkit for managing your observability infrastructure.
</p>
</div>
<div class="mx-auto max-w-7xl">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
<svg class="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Monitor Management</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Create, list, update, and delete monitors. Support for HTTP, TCP, UDP, ping, and custom monitors.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-red-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-red-50 ring-1 ring-red-200">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Incident Response</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Create, acknowledge, and resolve incidents. Add notes, change severity, and manage the full incident lifecycle.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
<svg class="h-6 w-6 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Status Pages</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Manage status pages, update component statuses, and post status updates from the command line.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-amber-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-50 ring-1 ring-amber-200">
<svg class="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Log Tailing</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Tail logs in real-time, search with filters, and pipe output to other tools for advanced analysis.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-50 ring-1 ring-violet-200">
<svg class="h-6 w-6 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">CI/CD Integration</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Integrate with GitHub Actions, GitLab CI, Jenkins, and more. Automate monitor creation on deployment.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-indigo-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-indigo-50 ring-1 ring-indigo-200">
<svg class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Scriptable Output</h3>
<p class="mt-4 text-gray-600 leading-relaxed">JSON output mode for scripting. Pipe output to jq, grep, or your own tools for custom workflows.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Install Section -->
<div class="relative bg-gray-900 py-24 sm:py-32 overflow-hidden">
<div class="relative mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 backdrop-blur border border-white/20 mb-6">
<svg class="h-4 w-4 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
<span class="text-sm font-medium text-white">Installation</span>
</div>
<h2 class="text-3xl font-bold tracking-tight text-white sm:text-4xl lg:text-5xl">
Install in one command
</h2>
<p class="mt-6 text-lg leading-8 text-gray-300">
Choose your preferred package manager and get started in seconds.
</p>
</div>
<div class="mx-auto max-w-2xl">
<div class="space-y-4">
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">npm</span>
</div>
<div class="font-mono text-sm text-emerald-400">
npm install -g oneuptime-cli
</div>
</div>
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Homebrew</span>
</div>
<div class="font-mono text-sm text-emerald-400">
brew install oneuptime/tap/oneuptime
</div>
</div>
<div class="rounded-xl bg-white/10 backdrop-blur border border-white/20 p-6">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-medium text-gray-300">Docker</span>
</div>
<div class="font-mono text-sm text-emerald-400">
docker run --rm oneuptime/cli monitor list
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('./Partials/enterprise-ready') -%>
<%- include('features-table') -%>
<%- include('cta') -%>
</main>
<%- include('footer') -%>
<%- include('./Partials/video-script') -%>
<script>
// Grid glow effect for CLI hero section
(function() {
const heroSection = document.getElementById('cli-hero-section');
const gridGlow = document.getElementById('cli-grid-glow');
if (heroSection && gridGlow) {
heroSection.addEventListener('mousemove', (e) => {
const rect = heroSection.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
gridGlow.style.setProperty('--mouse-x', `${x}px`);
gridGlow.style.setProperty('--mouse-y', `${y}px`);
gridGlow.style.opacity = '1';
});
heroSection.addEventListener('mouseleave', () => {
gridGlow.style.opacity = '0';
});
}
})();
</script>
</body>
</html>

View File

@@ -133,6 +133,12 @@
<li><a href="/solutions/platform" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">Platform</a></li>
<li><a href="/solutions/developers" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">Developers</a></li>
</ul>
<h3 class="text-sm font-semibold text-gray-900 mt-8">Tools</h3>
<ul role="list" class="mt-4 space-y-3">
<li><a href="/tool/mcp-server" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">MCP Server</a></li>
<li><a href="/tool/cli" class="text-sm text-gray-600 hover:text-gray-900 transition-colors duration-200">CLI</a></li>
</ul>
</div>
</div>

607
Home/Views/mcp-server.ejs Normal file
View File

@@ -0,0 +1,607 @@
<!DOCTYPE html>
<html lang="en" id="home">
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<head>
<title>OneUptime | MCP Server - Model Context Protocol for AI Agents</title>
<meta name="description"
content="Connect AI agents and LLMs to your OneUptime observability data via Model Context Protocol (MCP). Query incidents, monitors, logs, metrics, and traces directly from your AI tools.">
<%- include('head', {
enableGoogleTagManager: typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false
}) -%>
</head>
<body>
<%- include('nav') -%>
<main id="main-content">
<!-- Hero Section -->
<div class="relative isolate overflow-hidden bg-white" id="mcp-hero-section">
<!-- Subtle grid pattern background -->
<div class="absolute inset-0 -z-10 h-full w-full bg-white bg-[linear-gradient(to_right,#f0f0f0_1px,transparent_1px),linear-gradient(to_bottom,#f0f0f0_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_110%)]"></div>
<!-- Grid glow effect that follows cursor -->
<div id="mcp-grid-glow" class="absolute inset-0 -z-9 pointer-events-none" style="opacity: 0; transition: opacity 0.3s ease-out; background: linear-gradient(to right, rgba(14, 165, 233, 0.3) 1px, transparent 1px), linear-gradient(to bottom, rgba(14, 165, 233, 0.3) 1px, transparent 1px); background-size: 4rem 4rem; -webkit-mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%); mask-image: radial-gradient(circle 250px at var(--mouse-x, 50%) var(--mouse-y, 50%), black 0%, transparent 100%);"></div>
<div class="py-20 sm:py-28 lg:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-3xl text-center">
<!-- Minimal badge -->
<p class="text-sm font-medium text-sky-600 mb-4">Model Context Protocol</p>
<h1 class="text-4xl font-semibold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl">
Connect AI agents to your observability data
</h1>
<p class="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
OneUptime MCP Server lets AI agents and LLMs query your incidents, monitors, logs, metrics, and traces directly. Give your AI tools real-time context about your infrastructure.
</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<a href="/accounts/register"
class="rounded-lg bg-gray-900 px-5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-800 transition-colors">
Get started
</a>
<a href="/docs/mcp-server" class="text-sm font-semibold text-gray-900 hover:text-gray-600 transition-colors">
Read docs <span aria-hidden="true">&rarr;</span>
</a>
</div>
<!-- Subtle feature list -->
<div class="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm text-gray-500">
<span>Incidents & Monitors</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Logs & Metrics</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Traces & Spans</span>
<span class="hidden sm:inline text-gray-300">|</span>
<span>Open Protocol</span>
</div>
</div>
<!-- Terminal mockup -->
<div class="mt-16 sm:mt-20">
<div class="relative mx-auto max-w-3xl">
<div class="rounded-xl bg-gray-900 p-1.5 ring-1 ring-gray-900/10 shadow-2xl">
<!-- Terminal header -->
<div class="flex items-center gap-2 px-4 py-3 border-b border-gray-700/50">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<span class="text-gray-400 text-xs font-mono ml-2">MCP Server Configuration</span>
</div>
<!-- Terminal content -->
<div class="p-6 font-mono text-sm">
<div class="text-gray-400">// Add to your MCP client config</div>
<div class="mt-2 text-gray-300">{</div>
<div class="text-gray-300 ml-4">"<span class="text-sky-400">mcpServers</span>": {</div>
<div class="text-gray-300 ml-8">"<span class="text-sky-400">oneuptime</span>": {</div>
<div class="text-gray-300 ml-12">"<span class="text-emerald-400">url</span>": "<span class="text-amber-300">https://your-oneuptime.com/mcp</span>",</div>
<div class="text-gray-300 ml-12">"<span class="text-emerald-400">headers</span>": {</div>
<div class="text-gray-300 ml-16">"<span class="text-emerald-400">Authorization</span>": "<span class="text-amber-300">Bearer &lt;API_KEY&gt;</span>"</div>
<div class="text-gray-300 ml-12">}</div>
<div class="text-gray-300 ml-8">}</div>
<div class="text-gray-300 ml-4">}</div>
<div class="text-gray-300">}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('logo-roll') -%>
<!-- How It Works -->
<div class="relative bg-gray-50 py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<p class="text-sm font-medium text-sky-600 uppercase tracking-wide mb-3">How It Works</p>
<h2 class="text-3xl font-semibold tracking-tight text-gray-900 sm:text-4xl">
From AI query to actionable insight
</h2>
<p class="mt-4 text-lg text-gray-600">
MCP Server bridges the gap between your AI tools and your observability platform.
</p>
</div>
<div class="mx-auto max-w-5xl">
<!-- Connecting line for desktop -->
<div class="hidden lg:block relative">
<div class="absolute top-8 left-[calc(12.5%+24px)] right-[calc(12.5%+24px)] h-px bg-sky-200"></div>
</div>
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
<!-- Step 1 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">1</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Connect</h3>
<p class="text-sm text-gray-600 leading-relaxed">Point your AI tool to your OneUptime MCP Server endpoint with your API key.</p>
</div>
<!-- Step 2 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.625 9.75a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 01.778-.332 48.294 48.294 0 005.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">2</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Query</h3>
<p class="text-sm text-gray-600 leading-relaxed">AI agents ask questions about your infrastructure in natural language.</p>
</div>
<!-- Step 3 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">3</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Retrieve</h3>
<p class="text-sm text-gray-600 leading-relaxed">MCP Server fetches real-time data from your OneUptime instance.</p>
</div>
<!-- Step 4 -->
<div class="text-center">
<div class="relative inline-flex items-center justify-center h-16 w-16 rounded-full bg-sky-600 text-white mb-6 shadow-sm">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<span class="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-white text-xs font-semibold text-sky-600 ring-2 ring-sky-600">4</span>
</div>
<h3 class="text-base font-semibold text-gray-900 mb-2">Act</h3>
<p class="text-sm text-gray-600 leading-relaxed">AI responds with context-aware insights and can take actions on your behalf.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Why OneUptime MCP Section -->
<div class="relative overflow-hidden bg-white py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-2xl text-center mb-20">
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
</svg>
Why MCP Server
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
Your observability data, AI-ready
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Give AI agents the full context they need to understand and resolve incidents faster.
</p>
</div>
<!-- Feature Blocks -->
<div class="space-y-24 lg:space-y-32">
<!-- Feature 1: Query Incidents -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
<svg class="h-5 w-5 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<span class="text-sm font-semibold text-sky-600 uppercase tracking-wide">Incident Intelligence</span>
</div>
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">Query Incidents in Real-Time</h3>
<p class="text-lg text-gray-600 mb-6">
AI agents can list active incidents, get incident details, check monitor statuses, and understand the current state of your infrastructure without leaving their workflow.
</p>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
List active and resolved incidents
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Get monitor health status
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
View incident timelines and notes
</li>
</ul>
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
<div class="mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- AI Chat Mockup -->
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
<div class="bg-sky-600 px-4 py-3 flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center">
<svg class="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div>
<div class="text-white font-semibold text-sm">AI Agent + MCP</div>
<div class="text-sky-200 text-xs">Connected to OneUptime</div>
</div>
</div>
<div class="p-4 space-y-4 bg-gray-50">
<div class="flex justify-end">
<div class="bg-sky-600 text-white px-4 py-2 rounded-2xl rounded-br-md max-w-[80%] text-sm">
What incidents are currently active?
</div>
</div>
<div class="flex justify-start">
<div class="bg-white border border-gray-200 px-4 py-3 rounded-2xl rounded-bl-md max-w-[85%] shadow-sm">
<div class="text-sm text-gray-700 space-y-2">
<p>Found <span class="font-semibold text-sky-600">2 active incidents</span>:</p>
<div class="space-y-2">
<div class="bg-red-50 rounded-lg p-2 border border-red-100">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-red-500"></span>
<span class="text-xs font-medium text-red-700">Critical</span>
</div>
<p class="text-xs text-gray-600 mt-1">API Gateway - High error rate (5xx)</p>
</div>
<div class="bg-amber-50 rounded-lg p-2 border border-amber-100">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-amber-500"></span>
<span class="text-xs font-medium text-amber-700">Warning</span>
</div>
<p class="text-xs text-gray-600 mt-1">Database - High latency detected</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="px-4 py-3 border-t border-gray-100 bg-white">
<div class="flex items-center gap-2 bg-gray-50 rounded-full px-4 py-2 border border-gray-200">
<input type="text" placeholder="Ask about your infrastructure..." class="flex-1 bg-transparent text-sm text-gray-600 outline-none" disabled>
<button class="w-8 h-8 rounded-full bg-sky-600 flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Feature 2: Observability Data -->
<div class="lg:grid lg:grid-cols-2 lg:gap-16 lg:items-center">
<div class="relative order-2 lg:order-1 mt-12 lg:mt-0">
<div class="relative">
<div class="absolute -inset-4 bg-gray-100/50 rounded-3xl blur-2xl"></div>
<!-- Data Visualization -->
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden max-w-md mx-auto border border-gray-200">
<div class="bg-gray-50 px-4 py-2.5 flex items-center gap-3 border-b border-gray-100">
<div class="flex gap-1.5">
<div class="w-2.5 h-2.5 rounded-full bg-red-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-400"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-400"></div>
</div>
<span class="text-xs text-gray-500 font-medium">MCP Tools Available</span>
</div>
<div class="p-4 space-y-3">
<div class="flex items-center gap-3 p-3 rounded-lg bg-sky-50 border border-sky-100">
<div class="w-8 h-8 rounded-lg bg-sky-100 flex items-center justify-center">
<svg class="w-4 h-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">list_incidents</div>
<div class="text-xs text-gray-500">Query active and past incidents</div>
</div>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-emerald-50 border border-emerald-100">
<div class="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
<svg class="w-4 h-4 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">get_monitor_status</div>
<div class="text-xs text-gray-500">Check health of any monitor</div>
</div>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-amber-50 border border-amber-100">
<div class="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
<svg class="w-4 h-4 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">query_logs</div>
<div class="text-xs text-gray-500">Search and filter log entries</div>
</div>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-violet-50 border border-violet-100">
<div class="w-8 h-8 rounded-lg bg-violet-100 flex items-center justify-center">
<svg class="w-4 h-4 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">get_metrics</div>
<div class="text-xs text-gray-500">Retrieve metric time series data</div>
</div>
</div>
<div class="flex items-center gap-3 p-3 rounded-lg bg-rose-50 border border-rose-100">
<div class="w-8 h-8 rounded-lg bg-rose-100 flex items-center justify-center">
<svg class="w-4 h-4 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
</div>
<div>
<div class="text-sm font-semibold text-gray-900">search_traces</div>
<div class="text-xs text-gray-500">Find and analyze distributed traces</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="relative order-1 lg:order-2">
<div class="flex items-center gap-4 mb-6">
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
<svg class="h-5 w-5 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>
</div>
<span class="text-sm font-semibold text-sky-600 uppercase tracking-wide">Full Data Access</span>
</div>
<h3 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-4">All Your Observability Tools</h3>
<p class="text-lg text-gray-600 mb-6">
MCP Server exposes a rich set of tools that let AI agents access every part of your observability stack - incidents, monitors, logs, metrics, traces, and more.
</p>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Structured tool definitions
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Type-safe parameters
</li>
<li class="flex items-center gap-3 text-gray-600">
<svg class="h-5 w-5 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Real-time data access
</li>
</ul>
<a href="/accounts/register" class="mt-6 inline-flex items-center gap-1.5 px-4 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
Get started
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Features Grid Section -->
<div class="relative bg-white py-24 sm:py-32 overflow-hidden">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
<svg class="h-4 w-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<span class="text-sm font-medium text-sky-700">Capabilities</span>
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Built for the AI-native workflow
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Everything your AI agents need to understand and act on your observability data.
</p>
</div>
<div class="mx-auto max-w-7xl">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-sky-50 ring-1 ring-sky-200">
<svg class="h-6 w-6 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Incident Management</h3>
<p class="mt-4 text-gray-600 leading-relaxed">List, query, and get details about incidents. AI agents can understand the full context of any incident.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-emerald-50 ring-1 ring-emerald-200">
<svg class="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Monitor Status</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Query the health and status of all your monitors. Get uptime data, response times, and alert configurations.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-amber-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-50 ring-1 ring-amber-200">
<svg class="h-6 w-6 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Log Search</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Search and filter logs with full-text search. AI agents can correlate log entries with incidents and traces.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-50 ring-1 ring-violet-200">
<svg class="h-6 w-6 text-violet-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Metrics Queries</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Retrieve metric time series data. AI agents can analyze trends, detect anomalies, and understand performance patterns.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-rose-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-rose-50 ring-1 ring-rose-200">
<svg class="h-6 w-6 text-rose-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Distributed Traces</h3>
<p class="mt-4 text-gray-600 leading-relaxed">Search and analyze distributed traces across services. Follow requests through your entire stack.</p>
</div>
</div>
<div class="group relative">
<div class="relative h-full rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-300 hover:border-indigo-200 hover:shadow-lg">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-indigo-50 ring-1 ring-indigo-200">
<svg class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<h3 class="mt-6 text-xl font-semibold text-gray-900">Secure by Default</h3>
<p class="mt-4 text-gray-600 leading-relaxed">API key authentication with fine-grained permissions. Control exactly what data AI agents can access.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Compatible AI Tools -->
<div class="relative bg-gray-50 py-24 sm:py-32 overflow-hidden">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto max-w-2xl text-center mb-16">
<div class="inline-flex items-center gap-2 rounded-full bg-sky-50 px-4 py-1.5 text-sm font-medium text-sky-700 ring-1 ring-inset ring-sky-600/20 mb-6">
<svg class="h-4 w-4 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.314a4.5 4.5 0 00-6.364-6.364L4.5 8.257m6.364 6.364l4.5-4.5" />
</svg>
<span class="text-sm font-medium text-sky-700">Compatible Tools</span>
</div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl lg:text-5xl">
Works with any MCP-compatible client
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Use OneUptime MCP Server with your favorite AI tools and IDE extensions.
</p>
</div>
<div class="mx-auto max-w-4xl">
<div class="grid grid-cols-2 gap-6 sm:grid-cols-4">
<div class="group relative">
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-sky-200 hover:shadow-lg">
<div class="text-xl font-bold text-gray-900">Claude</div>
<span class="text-sm text-gray-500 mt-1">Desktop & API</span>
</div>
</div>
<div class="group relative">
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-violet-200 hover:shadow-lg">
<div class="text-xl font-bold text-gray-900">Cursor</div>
<span class="text-sm text-gray-500 mt-1">AI IDE</span>
</div>
</div>
<div class="group relative">
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-blue-200 hover:shadow-lg">
<div class="text-xl font-bold text-gray-900">Windsurf</div>
<span class="text-sm text-gray-500 mt-1">AI IDE</span>
</div>
</div>
<div class="group relative">
<div class="relative flex flex-col items-center p-8 rounded-2xl bg-white border border-gray-200 transition-all duration-300 hover:border-emerald-200 hover:shadow-lg">
<div class="text-xl font-bold text-gray-900">Custom</div>
<span class="text-sm text-gray-500 mt-1">Any MCP Client</span>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('./Partials/enterprise-ready') -%>
<%- include('features-table') -%>
<%- include('cta') -%>
</main>
<%- include('footer') -%>
<%- include('./Partials/video-script') -%>
<script>
// Grid glow effect for MCP hero section
(function() {
const heroSection = document.getElementById('mcp-hero-section');
const gridGlow = document.getElementById('mcp-grid-glow');
if (heroSection && gridGlow) {
heroSection.addEventListener('mousemove', (e) => {
const rect = heroSection.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
gridGlow.style.setProperty('--mouse-x', `${x}px`);
gridGlow.style.setProperty('--mouse-y', `${y}px`);
gridGlow.style.opacity = '1';
});
heroSection.addEventListener('mouseleave', () => {
gridGlow.style.opacity = '0';
});
}
})();
</script>
</body>
</html>

6
MCP/package-lock.json generated
View File

@@ -1297,9 +1297,9 @@
}
},
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",

View File

@@ -1,7 +1,7 @@
{
"expo": {
"name": "OneUptime",
"slug": "oneuptime",
"slug": "oneuptime-on-call",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
@@ -11,7 +11,7 @@
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#FFFFFF"
"backgroundColor": "#0D1117"
},
"ios": {
"supportsTablet": true,
@@ -28,10 +28,23 @@
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0D1117"
},
"edgeToEdgeEnabled": true,
"package": "com.oneuptime.oncall"
"edgeToEdgeEnabled": false,
"package": "com.oneuptime.oncall",
"googleServicesFile": "./google-services.json",
"permissions": [
"android.permission.USE_BIOMETRIC",
"android.permission.USE_FINGERPRINT"
]
},
"plugins": [
[
"expo-splash-screen",
{
"backgroundColor": "#0D1117",
"image": "./assets/splash-icon.png",
"imageWidth": 200
}
],
[
"expo-notifications",
{
@@ -43,6 +56,12 @@
],
"web": {
"favicon": "./assets/favicon.png"
}
},
"extra": {
"eas": {
"projectId": "d9f87edc-1c3e-466f-b032-1ced7621aa8a"
}
},
"owner": "oneuptime"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 44 KiB

18
MobileApp/eas.json Normal file
View File

@@ -0,0 +1,18 @@
{
"cli": {
"version": ">= 3.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {}
},
"submit": {
"production": {}
}
}

Some files were not shown because too many files have changed in this diff Show More