Merge remote-tracking branch 'upstream/main' into permission-flag-definition-foundation
This commit is contained in:
@@ -60,7 +60,6 @@
|
||||
"packages/twenty-sdk",
|
||||
"packages/twenty-front-component-renderer",
|
||||
"packages/twenty-client-sdk",
|
||||
"packages/twenty-apps",
|
||||
"packages/twenty-cli",
|
||||
"packages/create-twenty-app",
|
||||
"packages/twenty-oxlint-rules",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -0,0 +1,14 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -0,0 +1,14 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/apps/getting-started
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard
|
||||
|
||||
## UUID requirement
|
||||
|
||||
- All generated UUIDs must be valid UUID v4.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab.
|
||||
@@ -20,7 +20,7 @@
|
||||
"test:watch": "vitest --config vitest.unit.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "2.1.0"
|
||||
"twenty-sdk": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
|
||||
+31
@@ -31,4 +31,35 @@ export default defineLogicFunction({
|
||||
required: ['teamId', 'title'],
|
||||
},
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
label: 'Create Linear Issue',
|
||||
inputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
issue: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
identifier: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
},
|
||||
error: { type: 'string' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
{"tags": ["scope:apps"]}
|
||||
@@ -5,6 +5,8 @@ import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orc
|
||||
import { renderDevUI } from '@/cli/utilities/dev/ui/components/dev-ui';
|
||||
import { DevUiStateManager } from '@/cli/utilities/dev/ui/dev-ui-state-manager';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import { checkServerVersionCompatibility } from '@/cli/utilities/version/check-server-version-compatibility';
|
||||
import { getVersionInfo } from '@/cli/utilities/version/get-version-info';
|
||||
|
||||
export type AppDevOptions = {
|
||||
appPath?: string;
|
||||
@@ -30,6 +32,10 @@ export class AppDevCommand {
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
|
||||
if (options.headless) {
|
||||
await checkServerVersionCompatibility();
|
||||
}
|
||||
|
||||
const config = await new ConfigService().getConfig();
|
||||
|
||||
const orchestratorState = new OrchestratorState({
|
||||
@@ -45,6 +51,10 @@ export class AppDevCommand {
|
||||
const { unmount } = await renderDevUI(uiStateManager);
|
||||
|
||||
this.unmountUI = unmount;
|
||||
|
||||
void getVersionInfo().then((info) =>
|
||||
orchestratorState.setVersionInfo(info),
|
||||
);
|
||||
}
|
||||
|
||||
this.orchestrator = new DevModeOrchestrator({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { appInstall } from '@/cli/operations/install';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { checkSdkVersionCompatibility } from '@/cli/utilities/version/check-sdk-version-compatibility';
|
||||
import { checkServerVersionCompatibility } from '@/cli/utilities/version/check-server-version-compatibility';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppInstallCommandOptions = {
|
||||
@@ -13,6 +14,7 @@ export class AppInstallCommand {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
await checkSdkVersionCompatibility(appPath);
|
||||
await checkServerVersionCompatibility();
|
||||
|
||||
console.log(chalk.blue('Installing application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}\n`));
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
checkServerHealth,
|
||||
detectLocalServer,
|
||||
} from '@/cli/utilities/server/detect-local-server';
|
||||
import { checkServerVersionCompatibility } from '@/cli/utilities/version/check-server-version-compatibility';
|
||||
import { execSync, spawn, spawnSync } from 'node:child_process';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -274,10 +275,19 @@ const innerServerStart = async (
|
||||
return { success: true, data: { port, url } };
|
||||
};
|
||||
|
||||
export const serverStart = (
|
||||
export const serverStart = async (
|
||||
options?: ServerStartOptions,
|
||||
): Promise<CommandResult<ServerStartResult>> =>
|
||||
runSafe(
|
||||
): Promise<CommandResult<ServerStartResult>> => {
|
||||
const result = await runSafe(
|
||||
() => innerServerStart(options),
|
||||
SERVER_ERROR_CODES.CONTAINER_START_FAILED,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const containerName = options?.test ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
||||
|
||||
await checkServerVersionCompatibility(containerName);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type CheckServerOrchestratorStepOutput } from '@/cli/utilities/dev/orch
|
||||
import { type StartWatchersOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
import { type SyncApplicationOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
|
||||
import { type UploadFilesOrchestratorStepOutput } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
|
||||
import { type VersionInfo } from '@/cli/utilities/version/version-info';
|
||||
import { type Manifest, SyncableEntity } from 'twenty-shared/application';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
@@ -114,6 +115,8 @@ export class OrchestratorState {
|
||||
|
||||
pipeline: OrchestratorStatePipeline;
|
||||
|
||||
versionInfo: VersionInfo | null;
|
||||
|
||||
entities: Map<string, OrchestratorStateEntityInfo>;
|
||||
events: OrchestratorStateEvent[];
|
||||
|
||||
@@ -172,10 +175,17 @@ export class OrchestratorState {
|
||||
appName: null,
|
||||
};
|
||||
|
||||
this.versionInfo = null;
|
||||
|
||||
this.entities = new Map();
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
setVersionInfo(versionInfo: VersionInfo): void {
|
||||
this.versionInfo = versionInfo;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
notify(): void {
|
||||
this.onChange?.();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
DevUiEntitySection,
|
||||
ENTITY_ORDER,
|
||||
} from '@/cli/utilities/dev/ui/components/dev-ui-entity-section';
|
||||
import { DevUiVersionRow } from '@/cli/utilities/dev/ui/components/dev-ui-version-row';
|
||||
import React from 'react';
|
||||
|
||||
export const DevUiSyncStatusIndicator = ({
|
||||
@@ -96,6 +97,7 @@ export const DevUiApplicationPanel = ({
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<DevUiVersionRow versionInfo={state.versionInfo} />
|
||||
</Box>
|
||||
|
||||
<Box marginLeft={2} flexDirection="column" marginTop={1}>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useInk } from '@/cli/utilities/dev/ui/dev-ui-ink-context';
|
||||
import { type VersionInfo } from '@/cli/utilities/version/version-info';
|
||||
import React from 'react';
|
||||
|
||||
const STALE_THRESHOLD_DAYS = 7;
|
||||
|
||||
export const DevUiVersionRow = ({
|
||||
versionInfo,
|
||||
}: {
|
||||
versionInfo: VersionInfo | null;
|
||||
}): React.ReactElement | null => {
|
||||
const { Box, Text } = useInk();
|
||||
|
||||
if (versionInfo === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
cliVersion,
|
||||
localServerVersion,
|
||||
latestServerVersion,
|
||||
isMinorOrMajorBehind,
|
||||
daysBehind,
|
||||
} = versionInfo;
|
||||
|
||||
const isStale =
|
||||
isMinorOrMajorBehind &&
|
||||
daysBehind !== null &&
|
||||
daysBehind > STALE_THRESHOLD_DAYS;
|
||||
|
||||
const isUpToDate =
|
||||
localServerVersion !== null &&
|
||||
latestServerVersion !== null &&
|
||||
localServerVersion === latestServerVersion;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text dimColor>Versions: </Text>
|
||||
<Text dimColor>CLI </Text>
|
||||
<Text>v{cliVersion}</Text>
|
||||
{localServerVersion !== null && (
|
||||
<>
|
||||
<Text dimColor> • Server </Text>
|
||||
<Text color={isStale ? 'yellow' : undefined}>
|
||||
v{localServerVersion}
|
||||
</Text>
|
||||
{isUpToDate && <Text color="green"> ✓ latest</Text>}
|
||||
{isStale && latestServerVersion !== null && (
|
||||
<>
|
||||
<Text color="yellow"> → v{latestServerVersion}</Text>
|
||||
{daysBehind !== null && (
|
||||
<Text dimColor> ({daysBehind}d behind)</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { compareSemver } from '@/cli/utilities/version/compare-semver';
|
||||
|
||||
describe('compareSemver', () => {
|
||||
it('returns 0 for equal versions', () => {
|
||||
expect(compareSemver([2, 4, 0], [2, 4, 0])).toBe(0);
|
||||
});
|
||||
|
||||
it('returns positive when a > b on major', () => {
|
||||
expect(compareSemver([3, 0, 0], [2, 99, 99])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('returns negative when a < b on minor', () => {
|
||||
expect(compareSemver([2, 3, 9], [2, 4, 0])).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('returns negative when a < b on patch', () => {
|
||||
expect(compareSemver([2, 4, 0], [2, 4, 1])).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { parseSemver } from '@/cli/utilities/version/parse-semver';
|
||||
|
||||
describe('parseSemver', () => {
|
||||
it('parses a plain semver', () => {
|
||||
expect(parseSemver('1.2.3')).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('strips a leading v', () => {
|
||||
expect(parseSemver('v2.4.0')).toEqual([2, 4, 0]);
|
||||
});
|
||||
|
||||
it('returns null for "latest"', () => {
|
||||
expect(parseSemver('latest')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a release-candidate tag', () => {
|
||||
expect(parseSemver('v2.4.0-rc')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for empty input', () => {
|
||||
expect(parseSemver('')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CONTAINER_NAME } from '@/cli/utilities/server/docker-container';
|
||||
import { getVersionInfo } from '@/cli/utilities/version/get-version-info';
|
||||
|
||||
const STALE_THRESHOLD_DAYS = 7;
|
||||
|
||||
export const checkServerVersionCompatibility = async (
|
||||
containerName: string = CONTAINER_NAME,
|
||||
): Promise<void> => {
|
||||
const info = await getVersionInfo(containerName);
|
||||
|
||||
if (
|
||||
info.localServerVersion === null ||
|
||||
info.latestServerVersion === null ||
|
||||
!info.isMinorOrMajorBehind ||
|
||||
info.daysBehind === null ||
|
||||
info.daysBehind <= STALE_THRESHOLD_DAYS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`⚠ Local Twenty server is v${info.localServerVersion} (${info.daysBehind} days behind v${info.latestServerVersion}).`,
|
||||
),
|
||||
);
|
||||
console.warn(chalk.dim(' Update with: yarn twenty server upgrade'));
|
||||
console.warn('');
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type SemverTuple } from '@/cli/utilities/version/semver-tuple';
|
||||
|
||||
export const compareSemver = (a: SemverTuple, b: SemverTuple): number => {
|
||||
if (a[0] !== b[0]) return a[0] - b[0];
|
||||
if (a[1] !== b[1]) return a[1] - b[1];
|
||||
|
||||
return a[2] - b[2];
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { CONTAINER_NAME } from '@/cli/utilities/server/docker-container';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const getLocalServerVersion = async (
|
||||
containerName: string = CONTAINER_NAME,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
'docker',
|
||||
[
|
||||
'inspect',
|
||||
'-f',
|
||||
'{{range .Config.Env}}{{println .}}{{end}}',
|
||||
containerName,
|
||||
],
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
|
||||
const match = stdout.match(/^APP_VERSION=(.+)$/m);
|
||||
|
||||
if (!match || match[1] === '0.0.0' || match[1] === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match[1].trim().replace(/^v/, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ensureDir } from '@/cli/utilities/file/fs-utils';
|
||||
import { compareSemver } from '@/cli/utilities/version/compare-semver';
|
||||
import { parseSemver } from '@/cli/utilities/version/parse-semver';
|
||||
import { type PublishedServerVersion } from '@/cli/utilities/version/published-server-version';
|
||||
|
||||
const CACHE_FILE = path.join(
|
||||
os.homedir(),
|
||||
'.twenty',
|
||||
'version-check-cache.json',
|
||||
);
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const FETCH_TIMEOUT_MS = 3000;
|
||||
const DOCKER_HUB_TAGS_URL =
|
||||
'https://hub.docker.com/v2/repositories/twentycrm/twenty-app-dev/tags?page_size=100';
|
||||
|
||||
type CacheFile = {
|
||||
fetchedAt: number;
|
||||
versions: Array<{ name: string; lastUpdatedAt: string }>;
|
||||
};
|
||||
|
||||
const readCache = async (): Promise<PublishedServerVersion[] | null> => {
|
||||
try {
|
||||
const content = await readFile(CACHE_FILE, 'utf-8');
|
||||
const parsed = JSON.parse(content) as CacheFile;
|
||||
|
||||
if (
|
||||
typeof parsed.fetchedAt !== 'number' ||
|
||||
!Array.isArray(parsed.versions)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Date.now() - parsed.fetchedAt > CACHE_TTL_MS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed.versions.map((entry) => ({
|
||||
name: entry.name,
|
||||
lastUpdatedAt: new Date(entry.lastUpdatedAt),
|
||||
}));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeCache = async (
|
||||
versions: PublishedServerVersion[],
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await ensureDir(path.dirname(CACHE_FILE));
|
||||
await writeFile(
|
||||
CACHE_FILE,
|
||||
JSON.stringify({
|
||||
fetchedAt: Date.now(),
|
||||
versions: versions.map(({ name, lastUpdatedAt }) => ({
|
||||
name,
|
||||
lastUpdatedAt: lastUpdatedAt.toISOString(),
|
||||
})),
|
||||
} satisfies CacheFile),
|
||||
);
|
||||
} catch {
|
||||
// Cache write failures shouldn't break the CLI
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromDockerHub = async (): Promise<
|
||||
PublishedServerVersion[] | null
|
||||
> => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(DOCKER_HUB_TAGS_URL, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
results?: Array<{ name?: unknown; last_updated?: unknown }>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(body.results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versions: PublishedServerVersion[] = [];
|
||||
|
||||
for (const tag of body.results) {
|
||||
if (
|
||||
typeof tag.name !== 'string' ||
|
||||
typeof tag.last_updated !== 'string' ||
|
||||
tag.name === 'latest' ||
|
||||
parseSemver(tag.name) === null
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastUpdatedAt = new Date(tag.last_updated);
|
||||
|
||||
if (Number.isNaN(lastUpdatedAt.getTime())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
versions.push({
|
||||
name: tag.name.replace(/^v/, ''),
|
||||
lastUpdatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
versions.sort((a, b) => {
|
||||
const aParsed = parseSemver(a.name);
|
||||
const bParsed = parseSemver(b.name);
|
||||
|
||||
if (aParsed === null || bParsed === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return compareSemver(bParsed, aParsed);
|
||||
});
|
||||
|
||||
return versions;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublishedServerVersions = async (): Promise<
|
||||
PublishedServerVersion[]
|
||||
> => {
|
||||
const cached = await readCache();
|
||||
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const fresh = await fetchFromDockerHub();
|
||||
|
||||
if (fresh === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await writeCache(fresh);
|
||||
|
||||
return fresh;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { CONTAINER_NAME } from '@/cli/utilities/server/docker-container';
|
||||
import { compareSemver } from '@/cli/utilities/version/compare-semver';
|
||||
import { getLocalServerVersion } from '@/cli/utilities/version/get-local-server-version';
|
||||
import { getPublishedServerVersions } from '@/cli/utilities/version/get-published-server-versions';
|
||||
import { parseSemver } from '@/cli/utilities/version/parse-semver';
|
||||
import { type VersionInfo } from '@/cli/utilities/version/version-info';
|
||||
import sdkPackageJson from '../../../../package.json';
|
||||
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Fallback for versions older than the most recent ~100 Docker Hub tags:
|
||||
// they can't be matched by name, so we report a sentinel age that always
|
||||
// exceeds any reasonable staleness threshold.
|
||||
const UNKNOWN_AGE_DAYS_BEHIND = 365;
|
||||
|
||||
export const getVersionInfo = async (
|
||||
containerName: string = CONTAINER_NAME,
|
||||
): Promise<VersionInfo> => {
|
||||
const cliVersion = sdkPackageJson.version;
|
||||
const [localServerVersion, publishedVersions] = await Promise.all([
|
||||
getLocalServerVersion(containerName),
|
||||
getPublishedServerVersions(),
|
||||
]);
|
||||
const latestServerVersion = publishedVersions[0]?.name ?? null;
|
||||
|
||||
const localParsed = localServerVersion
|
||||
? parseSemver(localServerVersion)
|
||||
: null;
|
||||
const latestParsed = latestServerVersion
|
||||
? parseSemver(latestServerVersion)
|
||||
: null;
|
||||
|
||||
const isMinorOrMajorBehind =
|
||||
localParsed !== null &&
|
||||
latestParsed !== null &&
|
||||
(localParsed[0] < latestParsed[0] ||
|
||||
(localParsed[0] === latestParsed[0] && localParsed[1] < latestParsed[1]));
|
||||
|
||||
const localPublishedAt =
|
||||
localParsed === null
|
||||
? undefined
|
||||
: publishedVersions.find((entry) => {
|
||||
const entryParsed = parseSemver(entry.name);
|
||||
|
||||
return (
|
||||
entryParsed !== null &&
|
||||
compareSemver(entryParsed, localParsed) === 0
|
||||
);
|
||||
})?.lastUpdatedAt;
|
||||
|
||||
const latestPublishedAt = publishedVersions[0]?.lastUpdatedAt;
|
||||
|
||||
let daysBehind: number | null = null;
|
||||
|
||||
if (latestPublishedAt && localPublishedAt) {
|
||||
daysBehind = Math.floor(
|
||||
(latestPublishedAt.getTime() - localPublishedAt.getTime()) / MS_PER_DAY,
|
||||
);
|
||||
} else if (latestPublishedAt && isMinorOrMajorBehind) {
|
||||
daysBehind = UNKNOWN_AGE_DAYS_BEHIND;
|
||||
}
|
||||
|
||||
return {
|
||||
cliVersion,
|
||||
localServerVersion,
|
||||
latestServerVersion,
|
||||
isMinorOrMajorBehind,
|
||||
daysBehind,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type SemverTuple } from '@/cli/utilities/version/semver-tuple';
|
||||
|
||||
export const parseSemver = (raw: string): SemverTuple | null => {
|
||||
const trimmed = raw.replace(/^v/, '').trim();
|
||||
const match = trimmed.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
parseInt(match[1], 10),
|
||||
parseInt(match[2], 10),
|
||||
parseInt(match[3], 10),
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type PublishedServerVersion = {
|
||||
name: string;
|
||||
lastUpdatedAt: Date;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type SemverTuple = [number, number, number];
|
||||
@@ -0,0 +1,7 @@
|
||||
export type VersionInfo = {
|
||||
cliVersion: string;
|
||||
localServerVersion: string | null;
|
||||
latestServerVersion: string | null;
|
||||
isMinorOrMajorBehind: boolean;
|
||||
daysBehind: number | null;
|
||||
};
|
||||
Reference in New Issue
Block a user