Skip to content

Commit f7e46e3

Browse files
authored
feat(core): add explicit cloud opt-out to CNW (#34580)
## Current Behavior The CNW cloud prompt is locked to auto-select deferred connection (CLOUD-4255), always generating a short URL but never writing nxCloudId to nx.json. Users have no explicit choice. ## Expected Behavior The cloud prompt now offers three explicit choices: - Yes: connect now, generate nxCloudId in nx.json, show strong completion message - Skip for now: deferred connection (no nxCloudId), still show short URL and update README - No: full opt-out, set neverConnectToCloud: true in nx.json, no URL, no README update, no cloud messaging **CLI args:** `--nxCloud=skip`, `--nxCloud=never` (new), `--nxCloud=yes`. Non-interactive defaults to skip. Closes CLOUD-4242
1 parent 5d64f72 commit f7e46e3

File tree

7 files changed

+99
-83
lines changed

7 files changed

+99
-83
lines changed

packages/create-nx-workspace/bin/create-nx-workspace.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
306306
const errorFile =
307307
error instanceof Error ? extractErrorFile(error) : undefined;
308308

309-
useCloud = argv.nxCloud !== 'skip';
309+
useCloud = argv.nxCloud !== 'skip' && argv.nxCloud !== 'never';
310310

311311
await recordStat({
312312
nxVersion,
@@ -451,7 +451,7 @@ async function main(parsedArgs: yargs.Arguments<Arguments>) {
451451
await recordStat({
452452
nxVersion,
453453
command: 'create-nx-workspace',
454-
useCloud: parsedArgs.nxCloud !== 'skip',
454+
useCloud: parsedArgs.nxCloud !== 'skip' && parsedArgs.nxCloud !== 'never',
455455
meta: {
456456
type: 'complete',
457457
flowVariant: getFlowVariant(),
@@ -604,7 +604,7 @@ async function normalizeArgsMiddleware(
604604
await recordStat({
605605
nxVersion,
606606
command: 'create-nx-workspace',
607-
useCloud: argv.nxCloud !== 'skip',
607+
useCloud: argv.nxCloud !== 'skip' && argv.nxCloud !== 'never',
608608
meta: {
609609
type: 'start',
610610
flowVariant: getFlowVariant(),
@@ -629,24 +629,36 @@ async function normalizeArgsMiddleware(
629629

630630
let nxCloud: string;
631631
let completionMessageKey: string | undefined;
632+
let skipCloudConnect = false;
633+
let neverConnectToCloud = false;
632634

633635
if (argv.skipGit === true) {
634636
nxCloud = 'skip';
635637
completionMessageKey = undefined;
636638
} else {
637-
// Always show cloud prompt with "full platform" message (CLOUD-4147)
638-
// Flow variant only affects completion banners, not this prompt
639-
nxCloud = await determineNxCloudV2(argv);
639+
const cloudChoice = await determineNxCloudV2(argv);
640+
if (cloudChoice === 'yes') {
641+
nxCloud = 'yes';
642+
skipCloudConnect = false;
643+
} else if (cloudChoice === 'skip') {
644+
nxCloud = 'yes';
645+
skipCloudConnect = true;
646+
} else {
647+
nxCloud = 'never';
648+
neverConnectToCloud = true;
649+
}
640650
completionMessageKey =
641-
nxCloud === 'skip' ? undefined : getCompletionMessageKeyForVariant();
651+
cloudChoice === 'never'
652+
? undefined
653+
: getCompletionMessageKeyForVariant();
642654
}
643655

644656
packageManager = argv.packageManager ?? detectInvokedPackageManager();
645657
Object.assign(argv, {
646658
nxCloud,
647-
useGitHub: nxCloud !== 'skip',
648-
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
649-
skipCloudConnect: nxCloud !== 'skip',
659+
useGitHub: nxCloud !== 'skip' && nxCloud !== 'never',
660+
skipCloudConnect,
661+
neverConnectToCloud,
650662
completionMessageKey,
651663
packageManager,
652664
defaultBase: 'main',
@@ -657,7 +669,7 @@ async function normalizeArgsMiddleware(
657669
await recordStat({
658670
nxVersion,
659671
command: 'create-nx-workspace',
660-
useCloud: nxCloud !== 'skip',
672+
useCloud: nxCloud !== 'skip' && nxCloud !== 'never',
661673
meta: {
662674
type: 'precreate',
663675
flowVariant: getFlowVariant(),
@@ -698,6 +710,7 @@ async function normalizeArgsMiddleware(
698710
let useGitHub: boolean | undefined;
699711
let completionMessageKey: string | undefined;
700712
let skipCloudConnect = false;
713+
let neverConnectToCloud = false;
701714

702715
if (argv.skipGit === true) {
703716
nxCloud = 'skip';
@@ -706,23 +719,38 @@ async function normalizeArgsMiddleware(
706719
// CLI arg provided: use existing flow (CI provider selection if needed)
707720
nxCloud = await determineNxCloud(argv);
708721
useGitHub =
709-
nxCloud === 'skip'
722+
nxCloud === 'skip' || nxCloud === 'never'
710723
? undefined
711724
: nxCloud === 'github' || (await determineIfGitHubWillBeUsed(argv));
725+
if (nxCloud === 'never') {
726+
neverConnectToCloud = true;
727+
}
712728
} else {
713729
// No CLI arg: use simplified prompt (same as template flow)
714-
nxCloud = await determineNxCloudV2(argv);
715-
useGitHub = nxCloud !== 'skip';
730+
const cloudChoice = await determineNxCloudV2(argv);
731+
if (cloudChoice === 'yes') {
732+
nxCloud = 'yes';
733+
skipCloudConnect = false;
734+
} else if (cloudChoice === 'skip') {
735+
nxCloud = 'yes';
736+
skipCloudConnect = true;
737+
} else {
738+
nxCloud = 'never';
739+
neverConnectToCloud = true;
740+
}
741+
useGitHub =
742+
nxCloud !== 'skip' && nxCloud !== 'never' ? true : undefined;
716743
completionMessageKey =
717-
nxCloud === 'skip' ? undefined : getCompletionMessageKeyForVariant();
718-
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
719-
skipCloudConnect = nxCloud !== 'skip';
744+
cloudChoice === 'never'
745+
? undefined
746+
: getCompletionMessageKeyForVariant();
720747
}
721748

722749
Object.assign(argv, {
723750
nxCloud,
724751
useGitHub,
725752
skipCloudConnect,
753+
neverConnectToCloud,
726754
completionMessageKey,
727755
packageManager,
728756
defaultBase,
@@ -734,7 +762,7 @@ async function normalizeArgsMiddleware(
734762
await recordStat({
735763
nxVersion,
736764
command: 'create-nx-workspace',
737-
useCloud: nxCloud !== 'skip',
765+
useCloud: nxCloud !== 'skip' && nxCloud !== 'never',
738766
meta: {
739767
type: 'precreate',
740768
flowVariant: getFlowVariant(),

packages/create-nx-workspace/src/create-workspace-options.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ export interface CreateWorkspaceOptions {
4242
cliName?: string; // Name of the CLI, used when displaying outputs. e.g. nx, Nx
4343
aiAgents?: Agent[]; // List of AI agents to configure
4444
/**
45-
* @description Skip cloud connection (variant 1 experiment - NXC-3628)
45+
* @description Skip cloud connection (deferred - show banner but don't write nxCloudId)
4646
* @default false
4747
*/
4848
skipCloudConnect?: boolean;
49+
/**
50+
* @description Set neverConnectToCloud in nx.json (full opt-out)
51+
* @default false
52+
*/
53+
neverConnectToCloud?: boolean;
4954
/**
5055
* @description Whether GitHub CLI (gh) is available on the system (for telemetry)
5156
*/

packages/create-nx-workspace/src/create-workspace.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
getNxCloudInfo,
1818
getSkippedNxCloudInfo,
1919
readNxCloudToken,
20+
setNeverConnectToCloud,
2021
} from './utils/nx/nx-cloud';
2122
import { output } from './utils/output';
2223
import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset';
@@ -135,8 +136,11 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
135136
}
136137

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

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

196+
// Handle "Never" opt-out: set neverConnectToCloud in nx.json
197+
if (options.neverConnectToCloud) {
198+
setNeverConnectToCloud(directory);
199+
}
200+
192201
let pushedToVcs = VcsPushStatus.SkippedGit;
193202

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

237-
if (nxCloud !== 'skip') {
246+
if (nxCloud !== 'skip' && nxCloud !== 'never') {
247+
// "Yes" or "Maybe later" — generate URL, update README, show banner
238248
const aiModeForCloud = isAiAgent();
239249
if (aiModeForCloud) {
240250
logProgress('configuring', 'Configuring Nx Cloud...');
241251
}
242-
// For variant 1 (skipCloudConnect=true): Skip readNxCloudToken() entirely
243-
// - We didn't call connectToNxCloudForTemplate(), so no token exists
244-
// - The spinner message "Checking Nx Cloud setup" would be misleading
245-
// - createNxCloudOnboardingUrl() uses GitHub flow which sends accessToken: null
246-
//
247-
// For variant 0: Read the token as before (cloud was connected)
252+
// skipCloudConnect=true (Maybe later): Skip readNxCloudToken() since no token exists
253+
// skipCloudConnect=false (Yes): Read the token as before (cloud was connected)
248254
const token = options.skipCloudConnect
249255
? undefined
250256
: readNxCloudToken(directory);
@@ -275,17 +281,18 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
275281
options.completionMessageKey,
276282
name
277283
);
278-
} else if (isTemplate && nxCloud === 'skip') {
279-
// Strip marker comments from README even when cloud is skipped
280-
// so users don't see raw <!-- BEGIN/END: nx-cloud --> markers
284+
} else if (isTemplate && (nxCloud === 'skip' || nxCloud === 'never')) {
285+
// Strip marker comments from README
281286
const readmeUpdated = addConnectUrlToReadme(directory, undefined);
282287
if (readmeUpdated && !skipGit && commit) {
283288
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
284289
await amendOrCommitReadme(directory, alreadyPushed);
285290
}
286291

287-
// Show nx connect message when user skips cloud in template flow
288-
nxCloudInfo = getSkippedNxCloudInfo();
292+
// Only show "nx connect" message for 'skip', not 'never'
293+
if (nxCloud === 'skip') {
294+
nxCloudInfo = getSkippedNxCloudInfo();
295+
}
289296
}
290297

291298
return {

packages/create-nx-workspace/src/internal-utils/prompts.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,23 @@ export async function determineNxCloud(
3535

3636
export async function determineNxCloudV2(
3737
parsedArgs: yargs.Arguments<{ nxCloud?: string; interactive?: boolean }>
38-
): Promise<'github' | 'skip'> {
38+
): Promise<'yes' | 'skip' | 'never'> {
3939
// Provided via flag
4040
if (parsedArgs.nxCloud) {
41-
return parsedArgs.nxCloud === 'skip' ? 'skip' : 'github';
41+
if (parsedArgs.nxCloud === 'skip') return 'skip';
42+
if (parsedArgs.nxCloud === 'never') return 'never';
43+
return 'yes';
4244
}
4345

4446
// Non-interactive mode
4547
if (!parsedArgs.interactive || isCI()) {
4648
return 'skip';
4749
}
4850

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

5457
export async function determineIfGitHubWillBeUsed(

packages/create-nx-workspace/src/utils/nx/ab-testing.ts

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const NxCloudChoices = [
163163
'bitbucket-pipelines',
164164
'circleci',
165165
'skip',
166+
'never',
166167
'yes', // Deprecated but still handled
167168
];
168169

@@ -212,52 +213,14 @@ const messageOptions: Record<string, MessageData[]> = {
212213
* Simplified Cloud prompt for template flow
213214
*/
214215
setupNxCloudV2: [
215-
//{
216-
// code: 'cloud-v2-remote-cache-visit',
217-
// message: 'Enable remote caching with Nx Cloud?',
218-
// initial: 0,
219-
// choices: [
220-
// { value: 'yes', name: 'Yes' },
221-
// { value: 'skip', name: 'Skip' },
222-
// ],
223-
// footer:
224-
// '\nRemote caching makes your builds faster for development and in CI: https://nx.dev/ci/features/remote-cache',
225-
// fallback: undefined,
226-
// completionMessage: 'cache-setup',
227-
//},
228-
//{
229-
// code: 'cloud-v2-fast-ci-visit',
230-
// message: 'Speed up CI and reduce compute costs with Nx Cloud?',
231-
// initial: 0,
232-
// choices: [
233-
// { value: 'yes', name: 'Yes' },
234-
// { value: 'skip', name: 'Skip' },
235-
// ],
236-
// footer:
237-
// '\n70% faster CI, 60% less compute, Automatically fix broken PRs: https://nx.dev/nx-cloud',
238-
// fallback: undefined,
239-
// completionMessage: 'ci-setup',
240-
//},
241-
//{
242-
// code: 'cloud-v2-green-prs-visit',
243-
// message: 'Get to green PRs faster with Nx Cloud?',
244-
// initial: 0,
245-
// choices: [
246-
// { value: 'yes', name: 'Yes' },
247-
// { value: 'skip', name: 'Skip' },
248-
// ],
249-
// footer:
250-
// '\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud',
251-
// fallback: undefined,
252-
// completionMessage: 'ci-setup',
253-
//},
254216
{
255-
code: 'cloud-v2-full-platform-visit',
256-
message: 'Try the full Nx platform?',
217+
code: 'connect-to-cloud',
218+
message: 'Connect to Nx Cloud?',
257219
initial: 0,
258220
choices: [
259221
{ value: 'yes', name: 'Yes' },
260-
{ value: 'skip', name: 'Skip' },
222+
{ value: 'skip', name: 'Skip for now' },
223+
{ value: 'never', name: "No, don't ask again" },
261224
],
262225
footer:
263226
'\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud',

packages/create-nx-workspace/src/utils/nx/messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type BannerVariant = '0' | '2';
1111
* Generates a simple box banner with the setup URL.
1212
*/
1313
function generateSimpleBanner(url: string): string[] {
14-
const content = `Finish your set up here: ${url}`;
14+
const content = `Finish setup: ${url}`;
1515
// Add padding around content (3 spaces on each side)
1616
const innerWidth = content.length + 6;
1717
const horizontalBorder = '+' + '-'.repeat(innerWidth) + '+';

packages/create-nx-workspace/src/utils/nx/nx-cloud.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export type NxCloud =
1616
| 'azure'
1717
| 'bitbucket-pipelines'
1818
| 'circleci'
19-
| 'skip';
19+
| 'skip'
20+
| 'never';
2021

2122
export async function connectToNxCloudForTemplate(
2223
directory: string,
@@ -146,3 +147,12 @@ export function getSkippedNxCloudInfo() {
146147
out.success(getSkippedCloudMessage());
147148
return out.getOutput();
148149
}
150+
151+
export function setNeverConnectToCloud(directory: string): void {
152+
const { readFileSync, writeFileSync } = require('fs');
153+
const { join } = require('path');
154+
const nxJsonPath = join(directory, 'nx.json');
155+
const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf-8'));
156+
nxJson.neverConnectToCloud = true;
157+
writeFileSync(nxJsonPath, JSON.stringify(nxJson, null, 2) + '\n');
158+
}

0 commit comments

Comments
 (0)