Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions packages/create-nx-workspace/bin/create-nx-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
const errorFile =
error instanceof Error ? extractErrorFile(error) : undefined;

useCloud = argv.nxCloud !== 'skip';
useCloud = argv.nxCloud !== 'skip' && argv.nxCloud !== 'never';

await recordStat({
nxVersion,
Expand Down Expand Up @@ -451,7 +451,7 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
await recordStat({
nxVersion,
command: 'create-nx-workspace',
useCloud: parsedArgs.nxCloud !== 'skip',
useCloud: parsedArgs.nxCloud !== 'skip' && parsedArgs.nxCloud !== 'never',
meta: {
type: 'complete',
flowVariant: getFlowVariant(),
Expand Down Expand Up @@ -604,7 +604,7 @@ async function normalizeArgsMiddleware(
await recordStat({
nxVersion,
command: 'create-nx-workspace',
useCloud: argv.nxCloud !== 'skip',
useCloud: argv.nxCloud !== 'skip' && argv.nxCloud !== 'never',
meta: {
type: 'start',
flowVariant: getFlowVariant(),
Expand All @@ -629,24 +629,36 @@ async function normalizeArgsMiddleware(

let nxCloud: string;
let completionMessageKey: string | undefined;
let skipCloudConnect = false;
let neverConnectToCloud = false;

if (argv.skipGit === true) {
nxCloud = 'skip';
completionMessageKey = undefined;
} else {
// Always show cloud prompt with "full platform" message (CLOUD-4147)
// Flow variant only affects completion banners, not this prompt
nxCloud = await determineNxCloudV2(argv);
const cloudChoice = await determineNxCloudV2(argv);
if (cloudChoice === 'yes') {
nxCloud = 'yes';
skipCloudConnect = false;
} else if (cloudChoice === 'skip') {
nxCloud = 'yes';
skipCloudConnect = true;
} else {
nxCloud = 'never';
neverConnectToCloud = true;
}
completionMessageKey =
nxCloud === 'skip' ? undefined : getCompletionMessageKeyForVariant();
cloudChoice === 'never'
? undefined
: getCompletionMessageKeyForVariant();
}

packageManager = argv.packageManager ?? detectInvokedPackageManager();
Object.assign(argv, {
nxCloud,
useGitHub: nxCloud !== 'skip',
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
skipCloudConnect: nxCloud !== 'skip',
useGitHub: nxCloud !== 'skip' && nxCloud !== 'never',
skipCloudConnect,
neverConnectToCloud,
completionMessageKey,
packageManager,
defaultBase: 'main',
Expand All @@ -657,7 +669,7 @@ async function normalizeArgsMiddleware(
await recordStat({
nxVersion,
command: 'create-nx-workspace',
useCloud: nxCloud !== 'skip',
useCloud: nxCloud !== 'skip' && nxCloud !== 'never',
meta: {
type: 'precreate',
flowVariant: getFlowVariant(),
Expand Down Expand Up @@ -698,6 +710,7 @@ async function normalizeArgsMiddleware(
let useGitHub: boolean | undefined;
let completionMessageKey: string | undefined;
let skipCloudConnect = false;
let neverConnectToCloud = false;

if (argv.skipGit === true) {
nxCloud = 'skip';
Expand All @@ -706,23 +719,38 @@ async function normalizeArgsMiddleware(
// CLI arg provided: use existing flow (CI provider selection if needed)
nxCloud = await determineNxCloud(argv);
useGitHub =
nxCloud === 'skip'
nxCloud === 'skip' || nxCloud === 'never'
? undefined
: nxCloud === 'github' || (await determineIfGitHubWillBeUsed(argv));
if (nxCloud === 'never') {
neverConnectToCloud = true;
}
} else {
// No CLI arg: use simplified prompt (same as template flow)
nxCloud = await determineNxCloudV2(argv);
useGitHub = nxCloud !== 'skip';
const cloudChoice = await determineNxCloudV2(argv);
if (cloudChoice === 'yes') {
nxCloud = 'yes';
skipCloudConnect = false;
} else if (cloudChoice === 'skip') {
nxCloud = 'yes';
skipCloudConnect = true;
} else {
nxCloud = 'never';
neverConnectToCloud = true;
}
useGitHub =
nxCloud !== 'skip' && nxCloud !== 'never' ? true : undefined;
completionMessageKey =
nxCloud === 'skip' ? undefined : getCompletionMessageKeyForVariant();
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
skipCloudConnect = nxCloud !== 'skip';
cloudChoice === 'never'
? undefined
: getCompletionMessageKeyForVariant();
}

Object.assign(argv, {
nxCloud,
useGitHub,
skipCloudConnect,
neverConnectToCloud,
completionMessageKey,
packageManager,
defaultBase,
Expand All @@ -734,7 +762,7 @@ async function normalizeArgsMiddleware(
await recordStat({
nxVersion,
command: 'create-nx-workspace',
useCloud: nxCloud !== 'skip',
useCloud: nxCloud !== 'skip' && nxCloud !== 'never',
meta: {
type: 'precreate',
flowVariant: getFlowVariant(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ export interface CreateWorkspaceOptions {
cliName?: string; // Name of the CLI, used when displaying outputs. e.g. nx, Nx
aiAgents?: Agent[]; // List of AI agents to configure
/**
* @description Skip cloud connection (variant 1 experiment - NXC-3628)
* @description Skip cloud connection (deferred - show banner but don't write nxCloudId)
* @default false
*/
skipCloudConnect?: boolean;
/**
* @description Set neverConnectToCloud in nx.json (full opt-out)
* @default false
*/
neverConnectToCloud?: boolean;
/**
* @description Whether GitHub CLI (gh) is available on the system (for telemetry)
*/
Expand Down
37 changes: 22 additions & 15 deletions packages/create-nx-workspace/src/create-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getNxCloudInfo,
getSkippedNxCloudInfo,
readNxCloudToken,
setNeverConnectToCloud,
} from './utils/nx/nx-cloud';
import { output } from './utils/output';
import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset';
Expand Down Expand Up @@ -135,8 +136,11 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
}

// Connect to Nx Cloud for template flow
// For variant 1 (NXC-3628): Skip connection, use GitHub flow for URL generation
if (nxCloud !== 'skip' && !options.skipCloudConnect) {
if (
nxCloud !== 'skip' &&
nxCloud !== 'never' &&
!options.skipCloudConnect
) {
await connectToNxCloudForTemplate(
directory,
'create-nx-workspace',
Expand Down Expand Up @@ -184,11 +188,16 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(

// Generate CI for preset flow (not template)
// When nxCloud === 'yes' (from simplified prompt), use GitHub as the CI provider
if (nxCloud !== 'skip' && !isTemplate) {
if (nxCloud !== 'skip' && nxCloud !== 'never' && !isTemplate) {
const ciProvider = nxCloud === 'yes' ? 'github' : nxCloud;
await setupCI(directory, ciProvider, packageManager);
}

// Handle "Never" opt-out: set neverConnectToCloud in nx.json
if (options.neverConnectToCloud) {
setNeverConnectToCloud(directory);
}

let pushedToVcs = VcsPushStatus.SkippedGit;

if (!skipGit) {
Expand Down Expand Up @@ -234,17 +243,14 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
let connectUrl: string | undefined;
let nxCloudInfo: string | undefined;

if (nxCloud !== 'skip') {
if (nxCloud !== 'skip' && nxCloud !== 'never') {
// "Yes" or "Maybe later" — generate URL, update README, show banner
const aiModeForCloud = isAiAgent();
if (aiModeForCloud) {
logProgress('configuring', 'Configuring Nx Cloud...');
}
// For variant 1 (skipCloudConnect=true): Skip readNxCloudToken() entirely
// - We didn't call connectToNxCloudForTemplate(), so no token exists
// - The spinner message "Checking Nx Cloud setup" would be misleading
// - createNxCloudOnboardingUrl() uses GitHub flow which sends accessToken: null
//
// For variant 0: Read the token as before (cloud was connected)
// skipCloudConnect=true (Maybe later): Skip readNxCloudToken() since no token exists
// skipCloudConnect=false (Yes): Read the token as before (cloud was connected)
const token = options.skipCloudConnect
? undefined
: readNxCloudToken(directory);
Expand Down Expand Up @@ -275,17 +281,18 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
options.completionMessageKey,
name
);
} else if (isTemplate && nxCloud === 'skip') {
// Strip marker comments from README even when cloud is skipped
// so users don't see raw <!-- BEGIN/END: nx-cloud --> markers
} else if (isTemplate && (nxCloud === 'skip' || nxCloud === 'never')) {
// Strip marker comments from README
const readmeUpdated = addConnectUrlToReadme(directory, undefined);
if (readmeUpdated && !skipGit && commit) {
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
await amendOrCommitReadme(directory, alreadyPushed);
}

// Show nx connect message when user skips cloud in template flow
nxCloudInfo = getSkippedNxCloudInfo();
// Only show "nx connect" message for 'skip', not 'never'
if (nxCloud === 'skip') {
nxCloudInfo = getSkippedNxCloudInfo();
}
}

return {
Expand Down
13 changes: 8 additions & 5 deletions packages/create-nx-workspace/src/internal-utils/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,23 @@ export async function determineNxCloud(

export async function determineNxCloudV2(
parsedArgs: yargs.Arguments<{ nxCloud?: string; interactive?: boolean }>
): Promise<'github' | 'skip'> {
): Promise<'yes' | 'skip' | 'never'> {
// Provided via flag
if (parsedArgs.nxCloud) {
return parsedArgs.nxCloud === 'skip' ? 'skip' : 'github';
if (parsedArgs.nxCloud === 'skip') return 'skip';
if (parsedArgs.nxCloud === 'never') return 'never';
return 'yes';
}

// Non-interactive mode
if (!parsedArgs.interactive || isCI()) {
return 'skip';
}

// Auto-select GitHub flow for deferred connection (variant 2 locked in - CLOUD-4255)
// Note: skipCloudConnect=true prevents actual connection, but we still get the banner
return 'github';
const result = await nxCloudPrompt('setupNxCloudV2');
if (result === 'never') return 'never';
if (result === 'skip') return 'skip';
return 'yes';
}

export async function determineIfGitHubWillBeUsed(
Expand Down
47 changes: 5 additions & 42 deletions packages/create-nx-workspace/src/utils/nx/ab-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const NxCloudChoices = [
'bitbucket-pipelines',
'circleci',
'skip',
'never',
'yes', // Deprecated but still handled
];

Expand Down Expand Up @@ -212,52 +213,14 @@ const messageOptions: Record<string, MessageData[]> = {
* Simplified Cloud prompt for template flow
*/
setupNxCloudV2: [
//{
// code: 'cloud-v2-remote-cache-visit',
// message: 'Enable remote caching with Nx Cloud?',
// initial: 0,
// choices: [
// { value: 'yes', name: 'Yes' },
// { value: 'skip', name: 'Skip' },
// ],
// footer:
// '\nRemote caching makes your builds faster for development and in CI: https://nx.dev/ci/features/remote-cache',
// fallback: undefined,
// completionMessage: 'cache-setup',
//},
//{
// code: 'cloud-v2-fast-ci-visit',
// message: 'Speed up CI and reduce compute costs with Nx Cloud?',
// initial: 0,
// choices: [
// { value: 'yes', name: 'Yes' },
// { value: 'skip', name: 'Skip' },
// ],
// footer:
// '\n70% faster CI, 60% less compute, Automatically fix broken PRs: https://nx.dev/nx-cloud',
// fallback: undefined,
// completionMessage: 'ci-setup',
//},
//{
// code: 'cloud-v2-green-prs-visit',
// message: 'Get to green PRs faster with Nx Cloud?',
// initial: 0,
// choices: [
// { value: 'yes', name: 'Yes' },
// { value: 'skip', name: 'Skip' },
// ],
// footer:
// '\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud',
// fallback: undefined,
// completionMessage: 'ci-setup',
//},
{
code: 'cloud-v2-full-platform-visit',
message: 'Try the full Nx platform?',
code: 'connect-to-cloud',
message: 'Connect to Nx Cloud?',
initial: 0,
choices: [
{ value: 'yes', name: 'Yes' },
{ value: 'skip', name: 'Skip' },
{ value: 'skip', name: 'Skip for now' },
{ value: 'never', name: "No, don't ask again" },
],
footer:
'\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud',
Expand Down
2 changes: 1 addition & 1 deletion packages/create-nx-workspace/src/utils/nx/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type BannerVariant = '0' | '2';
* Generates a simple box banner with the setup URL.
*/
function generateSimpleBanner(url: string): string[] {
const content = `Finish your set up here: ${url}`;
const content = `Finish setup: ${url}`;
// Add padding around content (3 spaces on each side)
const innerWidth = content.length + 6;
const horizontalBorder = '+' + '-'.repeat(innerWidth) + '+';
Expand Down
12 changes: 11 additions & 1 deletion packages/create-nx-workspace/src/utils/nx/nx-cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type NxCloud =
| 'azure'
| 'bitbucket-pipelines'
| 'circleci'
| 'skip';
| 'skip'
| 'never';

export async function connectToNxCloudForTemplate(
directory: string,
Expand Down Expand Up @@ -146,3 +147,12 @@ export function getSkippedNxCloudInfo() {
out.success(getSkippedCloudMessage());
return out.getOutput();
}

export function setNeverConnectToCloud(directory: string): void {
const { readFileSync, writeFileSync } = require('fs');
const { join } = require('path');
const nxJsonPath = join(directory, 'nx.json');
const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf-8'));
nxJson.neverConnectToCloud = true;
writeFileSync(nxJsonPath, JSON.stringify(nxJson, null, 2) + '\n');
}
Loading