diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index d40ff5022a9..7c082d0661f 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -306,7 +306,7 @@ export const commandsObject: yargs.Argv = yargs const errorFile = error instanceof Error ? extractErrorFile(error) : undefined; - useCloud = argv.nxCloud !== 'skip'; + useCloud = argv.nxCloud !== 'skip' && argv.nxCloud !== 'never'; await recordStat({ nxVersion, @@ -451,7 +451,7 @@ async function main(parsedArgs: yargs.Arguments) { await recordStat({ nxVersion, command: 'create-nx-workspace', - useCloud: parsedArgs.nxCloud !== 'skip', + useCloud: parsedArgs.nxCloud !== 'skip' && parsedArgs.nxCloud !== 'never', meta: { type: 'complete', flowVariant: getFlowVariant(), @@ -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(), @@ -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', @@ -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(), @@ -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'; @@ -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, @@ -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(), diff --git a/packages/create-nx-workspace/src/create-workspace-options.ts b/packages/create-nx-workspace/src/create-workspace-options.ts index 116b8a50949..c4f9b9c50dd 100644 --- a/packages/create-nx-workspace/src/create-workspace-options.ts +++ b/packages/create-nx-workspace/src/create-workspace-options.ts @@ -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) */ diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index 9b86c797276..5403fd0efe0 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -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'; @@ -135,8 +136,11 @@ export async function createWorkspace( } // 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', @@ -184,11 +188,16 @@ export async function createWorkspace( // 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) { @@ -234,17 +243,14 @@ export async function createWorkspace( 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); @@ -275,17 +281,18 @@ export async function createWorkspace( options.completionMessageKey, name ); - } else if (isTemplate && nxCloud === 'skip') { - // Strip marker comments from README even when cloud is skipped - // so users don't see raw 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 { diff --git a/packages/create-nx-workspace/src/internal-utils/prompts.ts b/packages/create-nx-workspace/src/internal-utils/prompts.ts index bedd0b6dc3f..eefe8a8933e 100644 --- a/packages/create-nx-workspace/src/internal-utils/prompts.ts +++ b/packages/create-nx-workspace/src/internal-utils/prompts.ts @@ -35,10 +35,12 @@ 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 @@ -46,9 +48,10 @@ export async function determineNxCloudV2( 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( diff --git a/packages/create-nx-workspace/src/utils/nx/ab-testing.ts b/packages/create-nx-workspace/src/utils/nx/ab-testing.ts index 9c6b742fe62..c318558c0b1 100644 --- a/packages/create-nx-workspace/src/utils/nx/ab-testing.ts +++ b/packages/create-nx-workspace/src/utils/nx/ab-testing.ts @@ -163,6 +163,7 @@ export const NxCloudChoices = [ 'bitbucket-pipelines', 'circleci', 'skip', + 'never', 'yes', // Deprecated but still handled ]; @@ -212,52 +213,14 @@ const messageOptions: Record = { * 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', diff --git a/packages/create-nx-workspace/src/utils/nx/messages.ts b/packages/create-nx-workspace/src/utils/nx/messages.ts index 0ec56d24295..0fe3ce2706e 100644 --- a/packages/create-nx-workspace/src/utils/nx/messages.ts +++ b/packages/create-nx-workspace/src/utils/nx/messages.ts @@ -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) + '+'; diff --git a/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts b/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts index 75c96b6e165..b8e88dbac65 100644 --- a/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts +++ b/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts @@ -16,7 +16,8 @@ export type NxCloud = | 'azure' | 'bitbucket-pipelines' | 'circleci' - | 'skip'; + | 'skip' + | 'never'; export async function connectToNxCloudForTemplate( directory: string, @@ -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'); +}