e50adaff2d
## Summary The current Docker-not-running message is unhelpful in two ways: 1. It doesn't tell users **how** to start Docker 2. "try again" is meaningless because a first-time user doesn't yet know the command they just ran (they got here from `create-twenty-app`, not from typing `yarn twenty server start` themselves) **Before:** ``` Docker is not running. Please start Docker and try again. ``` **After (macOS example):** ``` Docker is not running. Start Docker: Run: open -a Docker (or launch Docker Desktop from Applications) Then retry: yarn twenty server start Don't have Docker? Install from https://docs.docker.com/get-docker/ ``` The platform-specific line is detected via `process.platform`: - `darwin` → `open -a Docker` + Docker Desktop fallback - `linux` → `sudo systemctl start docker` + Docker Desktop fallback - `win32` → "Launch Docker Desktop from the Start menu" - other → link to install docs The retry command is computed at the call site so it preserves the user's actual flags — `yarn twenty server start --test`, `yarn twenty server upgrade 2.2.0 --test`, etc. ## Why This came out of shadowing a first-time app developer who hit this error during `npx create-twenty-app`. They were stuck — the CLI told them to "try again" but they had only learned two commands so far (`create-twenty-app` and `yarn dev`), neither of which was the right one. Improving the message turns the error into a teaching moment. ## Test plan - [x] `npx nx typecheck twenty-sdk` passes - [x] `npx nx lint twenty-sdk` passes - [x] Manually verified rendered output for both `server start` and `server upgrade` flows on macOS - [ ] Verify message renders correctly on Linux/Windows in practice ## Possible follow-ups (out of scope) - Auto-launch Docker Desktop on macOS if installed (changes user state — separate PR) - Make the multi-line CLI error printer style only the first line in red, so guidance reads as default text rather than red Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
3.7 KiB
TypeScript
154 lines
3.7 KiB
TypeScript
import { SERVER_ERROR_CODES, type CommandResult } from '@/cli/types';
|
|
import { runSafe } from '@/cli/utilities/run-safe';
|
|
import {
|
|
checkDockerRunning,
|
|
CONTAINER_NAME,
|
|
containerExists,
|
|
getContainerDigest,
|
|
getContainerPort,
|
|
getDockerNotRunningMessage,
|
|
getImageDigest,
|
|
getImageForVersion,
|
|
TEST_CONTAINER_NAME,
|
|
} from '@/cli/utilities/server/docker-container';
|
|
import { execSync, spawnSync } from 'node:child_process';
|
|
|
|
export type ServerUpgradeOptions = {
|
|
version?: string;
|
|
test?: boolean;
|
|
onProgress?: (message: string) => void;
|
|
};
|
|
|
|
export type ServerUpgradeResult = {
|
|
image: string;
|
|
imageUpdated: boolean;
|
|
containerRecreated: boolean;
|
|
};
|
|
|
|
const innerServerUpgrade = async (
|
|
options: ServerUpgradeOptions = {},
|
|
): Promise<CommandResult<ServerUpgradeResult>> => {
|
|
const { version = 'latest', test: isTest, onProgress } = options;
|
|
|
|
if (!checkDockerRunning()) {
|
|
const retryCommand = [
|
|
'yarn twenty server upgrade',
|
|
version !== 'latest' ? version : null,
|
|
isTest ? '--test' : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: SERVER_ERROR_CODES.DOCKER_NOT_RUNNING,
|
|
message: getDockerNotRunningMessage(retryCommand),
|
|
},
|
|
};
|
|
}
|
|
|
|
const containerName = isTest ? TEST_CONTAINER_NAME : CONTAINER_NAME;
|
|
const image = getImageForVersion(version);
|
|
const hasContainer = containerExists(containerName);
|
|
const previousDigest = hasContainer
|
|
? getContainerDigest(containerName)
|
|
: null;
|
|
|
|
onProgress?.(`Pulling ${image}...`);
|
|
|
|
const pullResult = spawnSync('docker', ['pull', image], {
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
if (pullResult.status !== 0) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: SERVER_ERROR_CODES.IMAGE_UPGRADE_FAILED,
|
|
message: `Failed to pull image ${image}. Check that the version exists.`,
|
|
},
|
|
};
|
|
}
|
|
|
|
const pulledDigest = getImageDigest(image);
|
|
const imageUpdated = !previousDigest || previousDigest !== pulledDigest;
|
|
|
|
if (!hasContainer) {
|
|
onProgress?.('Image pulled. No existing container to upgrade.');
|
|
|
|
return {
|
|
success: true,
|
|
data: { image, imageUpdated, containerRecreated: false },
|
|
};
|
|
}
|
|
|
|
if (!imageUpdated) {
|
|
onProgress?.('Already up to date.');
|
|
|
|
return {
|
|
success: true,
|
|
data: { image, imageUpdated: false, containerRecreated: false },
|
|
};
|
|
}
|
|
|
|
const port = getContainerPort(containerName);
|
|
|
|
onProgress?.('Removing existing container...');
|
|
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' });
|
|
|
|
const volumeData = isTest
|
|
? 'twenty-app-dev-test-data'
|
|
: 'twenty-app-dev-data';
|
|
const volumeStorage = isTest
|
|
? 'twenty-app-dev-test-storage'
|
|
: 'twenty-app-dev-storage';
|
|
|
|
onProgress?.('Starting container with new image...');
|
|
|
|
const runResult = spawnSync(
|
|
'docker',
|
|
[
|
|
'run',
|
|
'-d',
|
|
'--name',
|
|
containerName,
|
|
'-p',
|
|
`${port}:${port}`,
|
|
'-e',
|
|
`NODE_PORT=${port}`,
|
|
'-e',
|
|
`SERVER_URL=http://localhost:${port}`,
|
|
'-v',
|
|
`${volumeData}:/data/postgres`,
|
|
'-v',
|
|
`${volumeStorage}:/app/packages/twenty-server/.local-storage`,
|
|
image,
|
|
],
|
|
{ stdio: 'inherit' },
|
|
);
|
|
|
|
if (runResult.status !== 0) {
|
|
return {
|
|
success: false,
|
|
error: {
|
|
code: SERVER_ERROR_CODES.IMAGE_UPGRADE_FAILED,
|
|
message: 'Failed to start container with new image.',
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: { image, imageUpdated: true, containerRecreated: true },
|
|
};
|
|
};
|
|
|
|
export const serverUpgrade = (
|
|
options?: ServerUpgradeOptions,
|
|
): Promise<CommandResult<ServerUpgradeResult>> =>
|
|
runSafe(
|
|
() => innerServerUpgrade(options),
|
|
SERVER_ERROR_CODES.IMAGE_UPGRADE_FAILED,
|
|
);
|