fix: drop domain from computed remote name when subdomain present (#22376)

## Context

Closes twentyhq/core-team-issues#2619

When adding a remote with `yarn twenty remote:add` and a URL like
`https://martin-s-workspace.twenty.com`, the computed remote name ended
up as `martin-s-workspace-twenty-com`. The apex domain (`twenty.com`)
should be dropped when a subdomain is present, so the name should be
`martin-s-workspace`.

## What changed

- Extracted `deriveRemoteName` from `remote/index.ts` into its own
module `remote/derive-remote-name.ts`.
- When the host has a subdomain (more than two labels), only the
subdomain labels are used (joined with dashes) — the apex domain is
dropped.
- Hosts with no subdomain keep the full host (`twenty.com` →
`twenty-com`).
- Single-label hosts like `localhost` are preserved.
- IPv4 addresses are kept intact (`127.0.0.1` → `127-0-0-1`).
- Invalid URLs still fall back to `remote`.

## Tests

Added `derive-remote-name.spec.ts` covering subdomain, multi-label
subdomain, apex-only host, `localhost`, IPv4, and invalid-URL cases. All
6 pass.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22376?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-07-01 08:33:31 +02:00
committed by GitHub
parent 1df00698cf
commit 06515408d1
3 changed files with 57 additions and 10 deletions
@@ -0,0 +1,31 @@
import { deriveRemoteName } from '@/cli/commands/remote/derive-remote-name';
describe('deriveRemoteName', () => {
it('should use only the subdomain when the host has one', () => {
expect(deriveRemoteName('https://martin-s-workspace.twenty.com')).toBe(
'martin-s-workspace',
);
});
it('should join multiple subdomain labels with dashes', () => {
expect(deriveRemoteName('https://app.staging.twenty.com')).toBe(
'app-staging',
);
});
it('should drop the tld when there is no subdomain', () => {
expect(deriveRemoteName('https://twenty.com')).toBe('twenty');
});
it('should handle single-label hosts like localhost', () => {
expect(deriveRemoteName('http://localhost:3000')).toBe('localhost');
});
it('should keep IPv4 addresses intact', () => {
expect(deriveRemoteName('http://127.0.0.1:3000')).toBe('127-0-0-1');
});
it('should fall back to "remote" for invalid URLs', () => {
expect(deriveRemoteName('not a url')).toBe('remote');
});
});
@@ -0,0 +1,25 @@
const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
export const deriveRemoteName = (url: string): string => {
try {
const { hostname } = new URL(url);
if (IPV4_REGEX.test(hostname)) {
return hostname.replace(/\./g, '-');
}
const labels = hostname.split('.');
if (labels.length > 2) {
return labels.slice(0, -2).join('-');
}
if (labels.length === 2) {
return labels[0];
}
return labels.join('-');
} catch {
return 'remote';
}
};
@@ -1,3 +1,4 @@
import { deriveRemoteName } from '@/cli/commands/remote/derive-remote-name';
import { authLogin } from '@/cli/operations/login';
import { authLoginOAuth } from '@/cli/operations/login-oauth';
import { ApiService } from '@/cli/utilities/api/api-service';
@@ -9,16 +10,6 @@ import type { Command } from 'commander';
import inquirer from 'inquirer';
import { normalizeUrl } from 'twenty-shared/utils';
const deriveRemoteName = (url: string): string => {
try {
const hostname = new URL(url).hostname;
return hostname.replace(/\./g, '-');
} catch {
return 'remote';
}
};
type AuthMethod = 'OAuth' | 'API key';
const authenticate = async (