Compare commits
10 commits
00a51f5e6a
...
cf696d200d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf696d200d | ||
|
|
bb128e3e16 | ||
|
|
63fac3b8c4 | ||
|
|
587fe76032 | ||
|
|
a18d90da86 | ||
|
|
febf98ea54 | ||
|
|
5747dc6fd8 | ||
|
|
d83944cc8d | ||
|
|
799b9cbfc5 | ||
|
|
a01b51fca5 |
20 changed files with 399 additions and 190 deletions
1
.github/workflows/build-branch.yml
vendored
1
.github/workflows/build-branch.yml
vendored
|
|
@ -397,6 +397,7 @@ jobs:
|
|||
with:
|
||||
tag_name: ${{ env.REL_VERSION }}
|
||||
name: ${{ env.REL_VERSION }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
draft: false
|
||||
prerelease: ${{ env.IS_PRERELEASE }}
|
||||
generate_release_notes: true
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RUN corepack enable pnpm
|
|||
|
||||
FROM base AS builder
|
||||
|
||||
RUN pnpm add -g turbo@2.8.12
|
||||
RUN pnpm add -g turbo@2.9.4
|
||||
|
||||
COPY . .
|
||||
|
||||
|
|
|
|||
|
|
@ -1118,7 +1118,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
|||
epoch = int(timezone.now().timestamp())
|
||||
|
||||
# Fetch all relevant issues in a single query
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids))
|
||||
issues = list(Issue.objects.filter(id__in=issue_ids, workspace__slug=slug, project_id=project_id))
|
||||
issues_dict = {str(issue.id): issue for issue in issues}
|
||||
issues_to_update = []
|
||||
|
||||
|
|
|
|||
|
|
@ -226,19 +226,34 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||
is_active=True,
|
||||
)
|
||||
|
||||
if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
|
||||
if "role" in request.data:
|
||||
# Only Admins can modify roles
|
||||
if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
{"error": "You do not have permission to update roles"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
if (
|
||||
"role" in request.data
|
||||
and int(request.data.get("role", project_member.role)) > requested_project_member.role
|
||||
and not is_workspace_admin
|
||||
):
|
||||
# Cannot modify a member whose role is equal to or higher than your own
|
||||
if project_member.role >= requested_project_member.role and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You cannot update a role that is higher than your own role"},
|
||||
{"error": "You cannot update the role of a member with a role equal to or higher than your own"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
new_role = int(request.data.get("role"))
|
||||
|
||||
# Cannot assign a role equal to or higher than your own
|
||||
if new_role >= requested_project_member.role and not is_workspace_admin:
|
||||
return Response(
|
||||
{"error": "You cannot assign a role equal to or higher than your own"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Cannot assign a role higher than the target's workspace role
|
||||
if workspace_role in [5] and new_role in [15, 20]:
|
||||
return Response(
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from bs4 import BeautifulSoup
|
|||
from urllib.parse import urlparse, urljoin
|
||||
import base64
|
||||
import ipaddress
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Tuple
|
||||
from typing import Optional
|
||||
from plane.db.models import IssueLink
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
|
@ -66,6 +66,52 @@ def validate_url_ip(url: str) -> None:
|
|||
MAX_REDIRECTS = 5
|
||||
|
||||
|
||||
def safe_get(
|
||||
url: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: int = 1,
|
||||
) -> Tuple[requests.Response, str]:
|
||||
"""
|
||||
Perform a GET request that validates every redirect hop against private IPs.
|
||||
Prevents SSRF by ensuring no redirect lands on a private/internal address.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
headers: Optional request headers
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
A tuple of (final Response object, final URL after redirects)
|
||||
|
||||
Raises:
|
||||
ValueError: If any URL in the redirect chain points to a private IP
|
||||
requests.RequestException: On network errors
|
||||
RuntimeError: If max redirects exceeded
|
||||
"""
|
||||
validate_url_ip(url)
|
||||
|
||||
current_url = url
|
||||
response = requests.get(
|
||||
current_url, headers=headers, timeout=timeout, allow_redirects=False
|
||||
)
|
||||
|
||||
redirect_count = 0
|
||||
while response.is_redirect:
|
||||
if redirect_count >= MAX_REDIRECTS:
|
||||
raise RuntimeError(f"Too many redirects for URL: {url}")
|
||||
redirect_url = response.headers.get("Location")
|
||||
if not redirect_url:
|
||||
break
|
||||
current_url = urljoin(current_url, redirect_url)
|
||||
validate_url_ip(current_url)
|
||||
redirect_count += 1
|
||||
response = requests.get(
|
||||
current_url, headers=headers, timeout=timeout, allow_redirects=False
|
||||
)
|
||||
|
||||
return response, current_url
|
||||
|
||||
|
||||
def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Crawls a URL to extract the title and favicon.
|
||||
|
|
@ -86,26 +132,8 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
|||
title = None
|
||||
final_url = url
|
||||
|
||||
validate_url_ip(final_url)
|
||||
|
||||
try:
|
||||
# Manually follow redirects to validate each URL before requesting
|
||||
redirect_count = 0
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
while response.is_redirect and redirect_count < MAX_REDIRECTS:
|
||||
redirect_url = response.headers.get("Location")
|
||||
if not redirect_url:
|
||||
break
|
||||
# Resolve relative redirects against current URL
|
||||
final_url = urljoin(final_url, redirect_url)
|
||||
# Validate the redirect target BEFORE making the request
|
||||
validate_url_ip(final_url)
|
||||
redirect_count += 1
|
||||
response = requests.get(final_url, headers=headers, timeout=1, allow_redirects=False)
|
||||
|
||||
if redirect_count >= MAX_REDIRECTS:
|
||||
logger.warning(f"Too many redirects for URL: {url}")
|
||||
response, final_url = safe_get(url, headers=headers)
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
title_tag = soup.find("title")
|
||||
|
|
@ -113,8 +141,10 @@ def crawl_work_item_link_title_and_favicon(url: str) -> Dict[str, Any]:
|
|||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"Failed to fetch HTML for title: {str(e)}")
|
||||
except (ValueError, RuntimeError) as e:
|
||||
logger.warning(f"URL validation failed: {str(e)}")
|
||||
|
||||
# Fetch and encode favicon using final URL (after redirects)
|
||||
# Fetch and encode favicon using final URL (after redirects) for correct relative href resolution
|
||||
favicon_base64 = fetch_and_encode_favicon(headers, soup, final_url)
|
||||
|
||||
# Prepare result
|
||||
|
|
@ -204,9 +234,7 @@ def fetch_and_encode_favicon(
|
|||
"favicon_base64": f"data:image/svg+xml;base64,{DEFAULT_FAVICON}",
|
||||
}
|
||||
|
||||
validate_url_ip(favicon_url)
|
||||
|
||||
response = requests.get(favicon_url, headers=headers, timeout=1)
|
||||
response, _ = safe_get(favicon_url, headers=headers)
|
||||
|
||||
# Get content type
|
||||
content_type = response.headers.get("content-type", "image/x-icon")
|
||||
|
|
|
|||
126
apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
Normal file
126
apps/api/plane/tests/unit/bg_tasks/test_work_item_link_task.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from plane.bgtasks.work_item_link_task import safe_get, validate_url_ip
|
||||
|
||||
|
||||
def _make_response(status_code=200, headers=None, is_redirect=False, content=b""):
|
||||
"""Create a mock requests.Response."""
|
||||
resp = MagicMock()
|
||||
resp.status_code = status_code
|
||||
resp.is_redirect = is_redirect
|
||||
resp.headers = headers or {}
|
||||
resp.content = content
|
||||
return resp
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestValidateUrlIp:
|
||||
"""Test validate_url_ip blocks private/internal IPs."""
|
||||
|
||||
def test_rejects_private_ip(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("192.168.1.1", 0))]
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
validate_url_ip("http://example.com")
|
||||
|
||||
def test_rejects_loopback(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("127.0.0.1", 0))]
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
validate_url_ip("http://example.com")
|
||||
|
||||
def test_rejects_non_http_scheme(self):
|
||||
with pytest.raises(ValueError, match="Only HTTP and HTTPS"):
|
||||
validate_url_ip("file:///etc/passwd")
|
||||
|
||||
def test_allows_public_ip(self):
|
||||
with patch("plane.bgtasks.work_item_link_task.socket.getaddrinfo") as mock_dns:
|
||||
mock_dns.return_value = [(None, None, None, None, ("93.184.216.34", 0))]
|
||||
validate_url_ip("https://example.com") # Should not raise
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSafeGet:
|
||||
"""Test safe_get follows redirects safely and blocks SSRF."""
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_returns_response_for_non_redirect(self, mock_validate, mock_get):
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
mock_get.return_value = final_resp
|
||||
|
||||
response, final_url = safe_get("https://example.com")
|
||||
|
||||
assert response is final_resp
|
||||
assert final_url == "https://example.com"
|
||||
mock_validate.assert_called_once_with("https://example.com")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_follows_redirect_and_validates_each_hop(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=301,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://other.com/page"},
|
||||
)
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
mock_get.side_effect = [redirect_resp, final_resp]
|
||||
|
||||
response, final_url = safe_get("https://example.com")
|
||||
|
||||
assert response is final_resp
|
||||
assert final_url == "https://other.com/page"
|
||||
# Should validate both the initial URL and the redirect target
|
||||
assert mock_validate.call_count == 2
|
||||
mock_validate.assert_any_call("https://example.com")
|
||||
mock_validate.assert_any_call("https://other.com/page")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_blocks_redirect_to_private_ip(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "http://192.168.1.1:8080"},
|
||||
)
|
||||
mock_get.return_value = redirect_resp
|
||||
# First call (initial URL) succeeds, second call (redirect target) fails
|
||||
mock_validate.side_effect = [None, ValueError("Access to private/internal networks is not allowed")]
|
||||
|
||||
with pytest.raises(ValueError, match="private/internal"):
|
||||
safe_get("https://evil.com/redirect")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_raises_on_too_many_redirects(self, mock_validate, mock_get):
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://example.com/loop"},
|
||||
)
|
||||
mock_get.return_value = redirect_resp
|
||||
|
||||
with pytest.raises(RuntimeError, match="Too many redirects"):
|
||||
safe_get("https://example.com/start")
|
||||
|
||||
@patch("plane.bgtasks.work_item_link_task.requests.get")
|
||||
@patch("plane.bgtasks.work_item_link_task.validate_url_ip")
|
||||
def test_succeeds_at_exact_max_redirects(self, mock_validate, mock_get):
|
||||
"""After exactly MAX_REDIRECTS hops, if the final response is 200, it should succeed."""
|
||||
redirect_resp = _make_response(
|
||||
status_code=302,
|
||||
is_redirect=True,
|
||||
headers={"Location": "https://example.com/next"},
|
||||
)
|
||||
final_resp = _make_response(status_code=200, content=b"OK")
|
||||
# 5 redirects then a 200
|
||||
mock_get.side_effect = [redirect_resp] * 5 + [final_resp]
|
||||
|
||||
response, final_url = safe_get("https://example.com/start")
|
||||
|
||||
assert response is final_resp
|
||||
assert not response.is_redirect
|
||||
|
|
@ -15,7 +15,7 @@ RUN apk update
|
|||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ARG TURBO_VERSION=2.8.12
|
||||
ARG TURBO_VERSION=2.9.4
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
RUN turbo prune --scope=live --docker
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ RUN corepack enable pnpm
|
|||
|
||||
FROM base AS builder
|
||||
|
||||
RUN pnpm add -g turbo@2.8.12
|
||||
RUN pnpm add -g turbo@2.9.4
|
||||
|
||||
COPY . .
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RUN apk add --no-cache libc6-compat
|
|||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.8.12
|
||||
ARG TURBO_VERSION=2.9.4
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ export function InboxIssueSnoozeModal(props: InboxIssueSnoozeModalProps) {
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
|
||||
<ModalCore
|
||||
isOpen={isOpen}
|
||||
handleClose={handleClose}
|
||||
position={EModalPosition.CENTER}
|
||||
width={EModalWidth.SM}
|
||||
className="w-auto"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-y-1 px-5 py-8 sm:p-6">
|
||||
<Calendar
|
||||
className="rounded-md border border-subtle p-3"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { xor } from "lodash-es";
|
||||
import { isEqual, xor } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// Plane imports
|
||||
|
|
@ -260,38 +260,57 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod
|
|||
}
|
||||
};
|
||||
|
||||
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
|
||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||
const handleCycleChange = async (data: Partial<TIssue> | undefined, payload: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !data?.project_id || !data?.id) return;
|
||||
// return if user is not trying to change the cycle, i.e
|
||||
// - cycle_id is not present in payload
|
||||
// - cycle_id is the same as the current cycle id
|
||||
if (!("cycle_id" in payload) || isEqual(data?.cycle_id, payload.cycle_id)) return;
|
||||
|
||||
try {
|
||||
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
|
||||
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
|
||||
const slug = workspaceSlug.toString();
|
||||
|
||||
// check if we should add/remove issue to/from cycle
|
||||
// Removing the cycle
|
||||
const currentCycleId = data?.cycle_id;
|
||||
if (currentCycleId && payload.cycle_id === null) {
|
||||
await issues.removeIssueFromCycle(slug, data.project_id, currentCycleId, data.id);
|
||||
fetchCycleDetails(slug, data.project_id, currentCycleId).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Adding the cycle
|
||||
const newCycleId = payload.cycle_id;
|
||||
if (newCycleId && newCycleId !== "" && (payload.cycle_id !== cycleId || storeType !== EIssuesStoreType.CYCLE)) {
|
||||
await addIssueToCycle(data as TBaseIssue, newCycleId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModuleChange = async (data: Partial<TIssue>, payload: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !data?.project_id || !data?.id) return;
|
||||
// return if user is not trying to change the module, i.e
|
||||
// - module_ids is not present in payload
|
||||
// - module_ids is not an array
|
||||
// - module_ids is the same as the current module ids
|
||||
if (
|
||||
payload.cycle_id &&
|
||||
payload.cycle_id !== "" &&
|
||||
(payload.cycle_id !== cycleId || storeType !== EIssuesStoreType.CYCLE)
|
||||
) {
|
||||
await addIssueToCycle(data as TBaseIssue, payload.cycle_id);
|
||||
}
|
||||
if (data.cycle_id && !payload.cycle_id && data.project_id) {
|
||||
await issues.removeIssueFromCycle(workspaceSlug.toString(), data.project_id, data.cycle_id, data.id);
|
||||
fetchCycleDetails(workspaceSlug.toString(), data.project_id, data.cycle_id);
|
||||
}
|
||||
!("module_ids" in payload) ||
|
||||
!Array.isArray(payload.module_ids) ||
|
||||
isEqual(data?.module_ids, payload.module_ids)
|
||||
)
|
||||
return;
|
||||
|
||||
if (data.module_ids && payload.module_ids && data.project_id) {
|
||||
const updatedModuleIds = xor(data.module_ids, payload.module_ids);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
|
||||
for (const moduleId of updatedModuleIds) {
|
||||
if (data.module_ids.includes(moduleId)) {
|
||||
if (data.module_ids?.includes(moduleId)) {
|
||||
modulesToRemove.push(moduleId);
|
||||
} else {
|
||||
modulesToAdd.push(moduleId);
|
||||
}
|
||||
}
|
||||
// update modules if there are modules to add or remove
|
||||
if (modulesToAdd.length > 0 || modulesToRemove.length > 0) {
|
||||
await issues.changeModulesInIssue(
|
||||
workspaceSlug.toString(),
|
||||
data.project_id,
|
||||
|
|
@ -300,8 +319,19 @@ export const CreateUpdateIssueModalBase = observer(function CreateUpdateIssueMod
|
|||
modulesToRemove
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// add other property values
|
||||
const handleUpdateIssue = async (payload: Partial<TIssue>): Promise<TIssue | undefined> => {
|
||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||
|
||||
try {
|
||||
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
|
||||
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
|
||||
|
||||
// Run cycle, module, and property changes sequentially to avoid
|
||||
// optimistic store writes from racing against each other.
|
||||
await handleCycleChange(data, payload);
|
||||
await handleModuleChange(data, payload);
|
||||
await handleCreateUpdatePropertyValues({
|
||||
issueId: data.id,
|
||||
issueTypeId: payload.type_id,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ FROM --platform=$BUILDPLATFORM tonistiigi/binfmt AS binfmt
|
|||
# **************************************************
|
||||
FROM node:22-alpine AS node
|
||||
|
||||
FROM artifacts.plane.so/makeplane/plane-frontend:${PLANE_VERSION} AS web-img
|
||||
FROM artifacts.plane.so/makeplane/plane-backend:${PLANE_VERSION} AS backend-img
|
||||
FROM artifacts.plane.so/makeplane/plane-space:${PLANE_VERSION} AS space-img
|
||||
FROM artifacts.plane.so/makeplane/plane-admin:${PLANE_VERSION} AS admin-img
|
||||
FROM artifacts.plane.so/makeplane/plane-live:${PLANE_VERSION} AS live-img
|
||||
FROM artifacts.plane.so/makeplane/plane-proxy:${PLANE_VERSION} AS proxy-img
|
||||
FROM makeplane/plane-frontend:${PLANE_VERSION} AS web-img
|
||||
FROM makeplane/plane-backend:${PLANE_VERSION} AS backend-img
|
||||
FROM makeplane/plane-space:${PLANE_VERSION} AS space-img
|
||||
FROM makeplane/plane-admin:${PLANE_VERSION} AS admin-img
|
||||
FROM makeplane/plane-live:${PLANE_VERSION} AS live-img
|
||||
FROM makeplane/plane-proxy:${PLANE_VERSION} AS proxy-img
|
||||
|
||||
# **************************************************
|
||||
# STAGE 1: Runner
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ docker run --name plane-aio --rm -it \
|
|||
-e AWS_ACCESS_KEY_ID=your-access-key \
|
||||
-e AWS_SECRET_ACCESS_KEY=your-secret-key \
|
||||
-e AWS_S3_BUCKET_NAME=your-bucket \
|
||||
artifacts.plane.so/makeplane/plane-aio-community:latest
|
||||
makeplane/plane-aio-community:latest
|
||||
```
|
||||
|
||||
### Example with IP Address
|
||||
|
|
@ -78,7 +78,7 @@ docker run --name myaio --rm -it \
|
|||
-e AWS_S3_BUCKET_NAME=plane-app \
|
||||
-e AWS_S3_ENDPOINT_URL=http://${MYIP}:19000 \
|
||||
-e FILE_SIZE_LIMIT=10485760 \
|
||||
artifacts.plane.so/makeplane/plane-aio-community:latest
|
||||
makeplane/plane-aio-community:latest
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ priority=10
|
|||
|
||||
|
||||
[program:space]
|
||||
directory=/app/space/apps/space/build/server
|
||||
command=sh -c "npx react-router-serve index.js"
|
||||
directory=/app/space/apps/space
|
||||
command=sh -c "npx react-router-serve ./build/server/index.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/app/logs/access/space.log
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ x-app-env: &app-env
|
|||
|
||||
services:
|
||||
web:
|
||||
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-frontend:${APP_RELEASE:-stable}
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
restart_policy:
|
||||
|
|
@ -71,7 +71,7 @@ services:
|
|||
- worker
|
||||
|
||||
space:
|
||||
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-space:${APP_RELEASE:-stable}
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
restart_policy:
|
||||
|
|
@ -82,7 +82,7 @@ services:
|
|||
- web
|
||||
|
||||
admin:
|
||||
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-admin:${APP_RELEASE:-stable}
|
||||
deploy:
|
||||
replicas: ${ADMIN_REPLICAS:-1}
|
||||
restart_policy:
|
||||
|
|
@ -92,7 +92,7 @@ services:
|
|||
- web
|
||||
|
||||
live:
|
||||
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-live:${APP_RELEASE:-stable}
|
||||
environment:
|
||||
<<: [*live-env, *redis-env]
|
||||
deploy:
|
||||
|
|
@ -104,7 +104,7 @@ services:
|
|||
- web
|
||||
|
||||
api:
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
deploy:
|
||||
replicas: ${API_REPLICAS:-1}
|
||||
|
|
@ -120,7 +120,7 @@ services:
|
|||
- plane-mq
|
||||
|
||||
worker:
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
deploy:
|
||||
replicas: ${WORKER_REPLICAS:-1}
|
||||
|
|
@ -137,7 +137,7 @@ services:
|
|||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
deploy:
|
||||
replicas: ${BEAT_WORKER_REPLICAS:-1}
|
||||
|
|
@ -154,7 +154,7 @@ services:
|
|||
- plane-mq
|
||||
|
||||
migrator:
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-stable}
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
deploy:
|
||||
replicas: 1
|
||||
|
|
@ -216,7 +216,7 @@ services:
|
|||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable}
|
||||
image: makeplane/plane-proxy:${APP_RELEASE:-stable}
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SCRIPT_DIR=$PWD
|
|||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE=stable
|
||||
export DOCKERHUB_USER=artifacts.plane.so/makeplane
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=${PULL_POLICY:-if_not_present}
|
||||
export GH_REPO=makeplane/plane
|
||||
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
|
||||
|
|
@ -690,7 +690,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
|
|||
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
|
||||
|
||||
if [ -z "$DOCKERHUB_USER" ]; then
|
||||
DOCKERHUB_USER=artifacts.plane.so/makeplane
|
||||
DOCKERHUB_USER=makeplane
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ SERVICE_FOLDER=plane-app
|
|||
SCRIPT_DIR=$PWD
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE="stable"
|
||||
export DOCKERHUB_USER=artifacts.plane.so/makeplane
|
||||
export DOCKERHUB_USER=makeplane
|
||||
|
||||
export GH_REPO=makeplane/plane
|
||||
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
|
||||
|
|
@ -595,7 +595,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
|
|||
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
|
||||
|
||||
if [ -z "$DOCKERHUB_USER" ]; then
|
||||
DOCKERHUB_USER=artifacts.plane.so/makeplane
|
||||
DOCKERHUB_USER=makeplane
|
||||
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"lint-staged": "16.2.7",
|
||||
"oxfmt": "0.35.0",
|
||||
"oxlint": "1.51.0",
|
||||
"turbo": "2.8.12"
|
||||
"turbo": "2.9.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts,json,css,md}": [
|
||||
|
|
@ -75,7 +75,8 @@
|
|||
"picomatch": "2.3.2",
|
||||
"yaml@1": "1.10.3",
|
||||
"yaml@2": "2.8.3",
|
||||
"path-to-regexp": "0.1.13"
|
||||
"path-to-regexp": "0.1.13",
|
||||
"defu": "6.1.5"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
|
|
|||
170
pnpm-lock.yaml
generated
170
pnpm-lock.yaml
generated
|
|
@ -103,7 +103,7 @@ overrides:
|
|||
qs: 6.14.2
|
||||
diff: 5.2.2
|
||||
webpack: 5.104.1
|
||||
lodash-es: 4.17.23
|
||||
lodash-es: 4.18.0
|
||||
'@isaacs/brace-expansion': 5.0.1
|
||||
lodash: 4.17.23
|
||||
markdown-it: 14.1.1
|
||||
|
|
@ -119,6 +119,7 @@ overrides:
|
|||
yaml@1: 1.10.3
|
||||
yaml@2: 2.8.3
|
||||
path-to-regexp: 0.1.13
|
||||
defu: 6.1.5
|
||||
|
||||
importers:
|
||||
|
||||
|
|
@ -137,8 +138,8 @@ importers:
|
|||
specifier: 1.51.0
|
||||
version: 1.51.0
|
||||
turbo:
|
||||
specifier: 2.8.12
|
||||
version: 2.8.12
|
||||
specifier: 2.9.4
|
||||
version: 2.9.4
|
||||
|
||||
apps/admin:
|
||||
dependencies:
|
||||
|
|
@ -194,8 +195,8 @@ importers:
|
|||
specifier: ^5.1.31
|
||||
version: 5.1.31
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -472,8 +473,8 @@ importers:
|
|||
specifier: ^5.1.31
|
||||
version: 5.1.31
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -644,8 +645,8 @@ importers:
|
|||
specifier: ^5.1.31
|
||||
version: 5.1.31
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -937,8 +938,8 @@ importers:
|
|||
specifier: ^4.3.2
|
||||
version: 4.3.2
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lowlight:
|
||||
specifier: ^3.0.0
|
||||
version: 3.3.0
|
||||
|
|
@ -1035,8 +1036,8 @@ importers:
|
|||
specifier: ^10.7.11
|
||||
version: 10.7.16
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
mobx:
|
||||
specifier: 'catalog:'
|
||||
version: 6.12.0
|
||||
|
|
@ -1216,8 +1217,8 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
mobx:
|
||||
specifier: 'catalog:'
|
||||
version: 6.12.0
|
||||
|
|
@ -1332,8 +1333,8 @@ importers:
|
|||
specifier: ^2.0.0
|
||||
version: 2.1.1
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -1456,8 +1457,8 @@ importers:
|
|||
specifier: ^10.1.2
|
||||
version: 10.1.2
|
||||
lodash-es:
|
||||
specifier: 4.17.23
|
||||
version: 4.17.23
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0
|
||||
lucide-react:
|
||||
specifier: 'catalog:'
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -4188,6 +4189,36 @@ packages:
|
|||
'@tokenizer/token@0.3.0':
|
||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||
|
||||
'@turbo/darwin-64@2.9.4':
|
||||
resolution: {integrity: sha512-ZSlPqJ5Vqg/wgVw8P3AOVCIosnbBilOxLq7TMz3MN/9U46DUYfdG2jtfevNDufyxyrg98pcPs/GBgDRaaids6g==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/darwin-arm64@2.9.4':
|
||||
resolution: {integrity: sha512-9cjTWe4OiNlFMSRggPNh+TJlRs7MS5FWrHc96MOzft5vESWjjpvaadYPv5ykDW7b45mVHOF2U/W+48LoX9USWw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/linux-64@2.9.4':
|
||||
resolution: {integrity: sha512-Cl1GjxqBXQ+r9KKowmXG+lhD1gclLp48/SE7NxL//66iaMytRw0uiphWGOkccD92iPiRjHLRUaA9lOTtgr5OCA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/linux-arm64@2.9.4':
|
||||
resolution: {integrity: sha512-j2hPAKVmGNN2EsKigEWD+43y9m7zaPhNAs6ptsyfq0u7evHHBAXAwOfv86OEMg/gvC+pwGip0i1CIm1bR1vYug==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/windows-64@2.9.4':
|
||||
resolution: {integrity: sha512-1jWPjCe9ZRmsDTXE7uzqfySNQspnUx0g6caqvwps+k/sc+fm9hC/4zRQKlXZLbVmP3Xxp601Ju71boegHdnYGw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@turbo/windows-arm64@2.9.4':
|
||||
resolution: {integrity: sha512-dlko15TQVu/BFYmIY018Y3covWMRQlUgAkD+OOk+Rokcfj6VY02Vv4mCfT/Zns6B4q8jGbOd6IZhnCFYsE8Viw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
|
|
@ -5259,8 +5290,8 @@ packages:
|
|||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
defu@6.1.5:
|
||||
resolution: {integrity: sha512-pwdBJxJuJXmqrLO6s0VBmfbRz+G7FUzkjldAsdi9Yrv86mPyzq0ll1o8+8gB4Gsr6GJHbK1Lh3ngllgTInDCjA==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
|
|
@ -6410,8 +6441,9 @@ packages:
|
|||
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
lodash-es@4.17.23:
|
||||
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
|
||||
lodash-es@4.18.0:
|
||||
resolution: {integrity: sha512-koAgswPPA+UTaPN64Etp+PGP+WT6oqOS2NMi5yDkMaiGw9qY4VxQbQF0mtKMyr4BlTznWyzePV5UpECTJQmSUA==}
|
||||
deprecated: Bad release. Please use lodash-es@4.17.23 instead.
|
||||
|
||||
lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
|
|
@ -8222,38 +8254,8 @@ packages:
|
|||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
turbo-darwin-64@2.8.12:
|
||||
resolution: {integrity: sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-darwin-arm64@2.8.12:
|
||||
resolution: {integrity: sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
turbo-linux-64@2.8.12:
|
||||
resolution: {integrity: sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
turbo-linux-arm64@2.8.12:
|
||||
resolution: {integrity: sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
turbo-windows-64@2.8.12:
|
||||
resolution: {integrity: sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
turbo-windows-arm64@2.8.12:
|
||||
resolution: {integrity: sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
turbo@2.8.12:
|
||||
resolution: {integrity: sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw==}
|
||||
turbo@2.9.4:
|
||||
resolution: {integrity: sha512-wZ/kMcZCuK5oEp7sXSSo/5fzKjP9I2EhoiarZjyCm2Ixk0WxFrC/h0gF3686eHHINoFQOOSWgB/pGfvkR8rkgQ==}
|
||||
hasBin: true
|
||||
|
||||
tween-functions@1.2.0:
|
||||
|
|
@ -11367,6 +11369,24 @@ snapshots:
|
|||
|
||||
'@tokenizer/token@0.3.0': {}
|
||||
|
||||
'@turbo/darwin-64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@turbo/darwin-arm64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-arm64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-arm64@2.9.4':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
|
@ -12535,7 +12555,7 @@ snapshots:
|
|||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
defu@6.1.4: {}
|
||||
defu@6.1.5: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
|
|
@ -13753,7 +13773,7 @@ snapshots:
|
|||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
lodash-es@4.17.23: {}
|
||||
lodash-es@4.18.0: {}
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
|
||||
|
|
@ -15021,7 +15041,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@icons/material': 0.2.4(react@18.3.1)
|
||||
lodash: 4.17.23
|
||||
lodash-es: 4.17.23
|
||||
lodash-es: 4.18.0
|
||||
material-colors: 1.2.6
|
||||
prop-types: 15.8.1
|
||||
react: 18.3.1
|
||||
|
|
@ -16027,32 +16047,14 @@ snapshots:
|
|||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
turbo-darwin-64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo-darwin-arm64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo-linux-64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo-linux-arm64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo-windows-64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo-windows-arm64@2.8.12:
|
||||
optional: true
|
||||
|
||||
turbo@2.8.12:
|
||||
turbo@2.9.4:
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 2.8.12
|
||||
turbo-darwin-arm64: 2.8.12
|
||||
turbo-linux-64: 2.8.12
|
||||
turbo-linux-arm64: 2.8.12
|
||||
turbo-windows-64: 2.8.12
|
||||
turbo-windows-arm64: 2.8.12
|
||||
'@turbo/darwin-64': 2.9.4
|
||||
'@turbo/darwin-arm64': 2.9.4
|
||||
'@turbo/linux-64': 2.9.4
|
||||
'@turbo/linux-arm64': 2.9.4
|
||||
'@turbo/windows-64': 2.9.4
|
||||
'@turbo/windows-arm64': 2.9.4
|
||||
|
||||
tween-functions@1.2.0: {}
|
||||
|
||||
|
|
@ -16079,7 +16081,7 @@ snapshots:
|
|||
unconfig@7.5.0:
|
||||
dependencies:
|
||||
'@quansync/fs': 1.0.0
|
||||
defu: 6.1.4
|
||||
defu: 6.1.5
|
||||
jiti: 2.6.1
|
||||
quansync: 1.0.0
|
||||
unconfig-core: 7.5.0
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ catalog:
|
|||
"@types/react": 18.3.11
|
||||
axios: 1.13.5
|
||||
express: 4.22.0
|
||||
lodash-es: 4.17.23
|
||||
lodash-es: 4.18.0
|
||||
lucide-react: 0.469.0
|
||||
mobx-react: 9.1.1
|
||||
mobx-utils: 6.0.8
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue