Skip to content

Commit e2a0560

Browse files
authored
feat(core): bring back cloud prompts and templates in CNW (#34887)
## Current Behavior CNW flow matches v22.1.3 (restored in #34671): no template prompt is shown, the cloud prompt uses simplified "Would you like remote caching?" wording, banner variant is locked to 0, and preset flow connects to cloud during workspace creation. ## Expected Behavior Restore the template prompt and cloud prompts that were removed in #34671: - **Template prompt**: "Which starter do you want to use?" with 5 choices (Minimal, React, Angular, NPM Packages, Custom) - **Cloud prompt**: `determineNxCloudV2` ("Connect to Nx Cloud?") for preset flow when no `--nxCloud` CLI arg; existing CI provider prompt when `--nxCloud` is explicitly provided - **Banner**: Variant 2 (box banner) for standard Nx Cloud URLs, variant 0 for enterprise - **Preset flow**: Deferred cloud connection (`nxCloud: 'skip'`) instead of connecting during workspace creation - **Push logic**: Push to GitHub for both `nxCloud === 'github'` and `nxCloud === 'yes'` - **Messages**: "Try the full Nx platform?" wording, GitHub repo link in push messages, "Your remote cache setup is almost complete." title Preserves all subsequent changes (analytics prompt from #34818, SANDBOX_FAILED fix, .gitignore updates). ## Related Issue(s) Fixes NXC-4096
1 parent b73cc69 commit e2a0560

File tree

8 files changed

+230
-138
lines changed

8 files changed

+230
-138
lines changed

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

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -717,19 +717,56 @@ async function normalizeArgsMiddleware(
717717
const aiAgents = await determineAiAgents(argv);
718718
const defaultBase = await determineDefaultBase(argv);
719719

720-
// NXC-4020: Restored v22.1.3 simple flow (CI prompt → caching fallback)
721-
const nxCloud =
722-
argv.skipGit === true ? 'skip' : await determineNxCloud(argv);
723-
const useGitHub =
724-
nxCloud === 'skip'
725-
? undefined
726-
: nxCloud === 'github' || (await determineIfGitHubWillBeUsed(argv));
720+
// Check if CLI arg was provided (use rawArgs to check original input)
721+
const cliNxCloudArgProvided = rawArgs.nxCloud !== undefined;
722+
723+
let nxCloud: string;
724+
let useGitHub: boolean | undefined;
725+
let completionMessageKey: string | undefined;
726+
let skipCloudConnect = false;
727+
let neverConnectToCloud = false;
728+
729+
if (argv.skipGit === true) {
730+
nxCloud = 'skip';
731+
useGitHub = undefined;
732+
} else if (cliNxCloudArgProvided) {
733+
// CLI arg provided: use existing flow (CI provider selection if needed)
734+
nxCloud = await determineNxCloud(argv);
735+
useGitHub =
736+
nxCloud === 'skip' || nxCloud === 'never'
737+
? undefined
738+
: nxCloud === 'github' || (await determineIfGitHubWillBeUsed(argv));
739+
if (nxCloud === 'never') {
740+
neverConnectToCloud = true;
741+
}
742+
} else {
743+
// No CLI arg: use simplified prompt (same as template flow)
744+
const cloudChoice = await determineNxCloudV2(argv);
745+
if (cloudChoice === 'yes') {
746+
nxCloud = 'yes';
747+
skipCloudConnect = false;
748+
} else if (cloudChoice === 'skip') {
749+
nxCloud = 'skip';
750+
} else {
751+
nxCloud = 'never';
752+
neverConnectToCloud = true;
753+
}
754+
useGitHub =
755+
nxCloud !== 'skip' && nxCloud !== 'never' ? true : undefined;
756+
completionMessageKey =
757+
cloudChoice === 'never'
758+
? undefined
759+
: getCompletionMessageKeyForVariant();
760+
}
727761

728762
const analytics = await determineAnalytics(argv);
729763

730764
Object.assign(argv, {
731765
nxCloud,
732766
useGitHub,
767+
skipCloudConnect,
768+
neverConnectToCloud,
769+
completionMessageKey,
733770
packageManager,
734771
defaultBase,
735772
aiAgents,
@@ -757,6 +794,10 @@ async function normalizeArgsMiddleware(
757794
if (e instanceof CnwError) {
758795
throw e;
759796
}
797+
// Enquirer throws an empty string when user presses Ctrl+C
798+
if (e === '') {
799+
process.exit(130);
800+
}
760801
const message = e instanceof Error ? e.message : String(e);
761802
throw new CnwError('UNKNOWN', message);
762803
}

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

Lines changed: 64 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
148148
);
149149
}
150150
} else {
151-
// NXC-4020: Preset flow — restored to match v22.1.3
151+
// Preset flow - existing behavior
152152
if (!preset) {
153153
throw new Error(
154154
'Preset is required when not using a template. Please provide --preset or --template.'
@@ -157,12 +157,14 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
157157
const tmpDir = await createSandbox(packageManager);
158158
const workspaceGlobs = getWorkspaceGlobsFromPreset(preset);
159159

160-
// NXC-4020: Pass actual nxCloud value (v22.1.3 behavior) so nxCloudId is set in nx.json
161-
// Previous: nxCloud: 'skip' override to defer cloud connection
160+
// nx new requires a preset currently. We should probably make it optional.
162161
directory = await createEmptyWorkspace<T>(tmpDir, name, packageManager, {
163162
...options,
164163
preset,
165164
workspaceGlobs,
165+
// We want new workspaces to have a short URL to finish Cloud onboarding, but not have nxCloudId set up since it will be handled as part of the onboarding flow.
166+
// This is skipping nxCloudId for the "custom" flow.
167+
nxCloud: 'skip',
166168
});
167169

168170
// Mark workspace as ready for SIGINT handler
@@ -195,41 +197,11 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
195197
setAnalyticsPreference(directory, options.analytics);
196198
}
197199

198-
// NXC-4020: Preset flow cloud handling matches v22.1.3 exactly:
199-
// 1. Read token (cloud was connected during createEmptyWorkspace)
200-
// 2. Setup CI for specific providers (not 'yes')
201-
// 3. Generate onboarding URL
202-
// 4. Git init with connectUrl
203-
// 5. Show completion message
204-
let connectUrl: string | undefined;
205-
let nxCloudInfo: string | undefined;
206-
207-
if (!isTemplate && nxCloud !== 'skip' && nxCloud !== 'never') {
208-
const token = readNxCloudToken(directory) as string;
209-
210-
if (nxCloud !== 'yes') {
211-
await setupCI(directory, nxCloud, packageManager);
212-
}
213-
214-
connectUrl = await createNxCloudOnboardingUrl(
215-
nxCloud,
216-
token,
217-
directory,
218-
useGitHub
219-
);
220-
221-
// Store for SIGINT handler
222-
cloudConnectUrl = connectUrl;
223-
}
224-
225-
// Template flow: CI setup and cloud connection handled separately
226-
if (
227-
isTemplate &&
228-
nxCloud !== 'skip' &&
229-
nxCloud !== 'never' &&
230-
nxCloud !== 'yes'
231-
) {
232-
await setupCI(directory, nxCloud, packageManager);
200+
// Generate CI for preset flow (not template)
201+
// When nxCloud === 'yes' (from simplified prompt), use GitHub as the CI provider
202+
if (nxCloud !== 'skip' && nxCloud !== 'never' && !isTemplate) {
203+
const ciProvider = nxCloud === 'yes' ? 'github' : nxCloud;
204+
await setupCI(directory, ciProvider, packageManager);
233205
}
234206

235207
let pushedToVcs = VcsPushStatus.SkippedGit;
@@ -241,12 +213,16 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
241213
}
242214

243215
try {
244-
// NXC-4020: Pass connectUrl to git init (v22.1.3 behavior)
245-
await initializeGitRepo(directory, { defaultBase, commit, connectUrl });
246-
247-
// NXC-4020: Only push for github CI provider (v22.1.3 behavior)
248-
// Previous: also pushed for nxCloud === 'yes'
249-
if (commit && !skipGitHubPush && nxCloud === 'github') {
216+
await initializeGitRepo(directory, { defaultBase, commit });
217+
218+
// Push to GitHub if commit was made, GitHub push is not skipped, and:
219+
// - CI provider is GitHub (preset flow with CLI arg), OR
220+
// - Nx Cloud enabled via simplified prompt (nxCloud === 'yes')
221+
if (
222+
commit &&
223+
!skipGitHubPush &&
224+
(nxCloud === 'github' || nxCloud === 'yes')
225+
) {
250226
pushedToVcs = await pushToGitHub(directory, {
251227
skipGitHubPush,
252228
name,
@@ -256,66 +232,72 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
256232
}
257233
} catch (e) {
258234
if (e instanceof Error) {
259-
if (!isAiAgent()) {
235+
if (!aiMode) {
260236
output.error({
261237
title: 'Could not initialize git repository',
262238
bodyLines: mapErrorToBodyLines(e),
263239
});
264240
}
241+
// In AI mode, error will be handled by the caller
265242
} else {
266243
console.error(e);
267244
}
268245
}
269246
}
270247

271-
// NXC-4020: Preset flow completion message matches v22.1.3
272-
if (!isTemplate && connectUrl) {
273-
nxCloudInfo = await getNxCloudInfo(
274-
nxCloud,
275-
connectUrl,
276-
pushedToVcs,
277-
rawArgs?.nxCloud
278-
);
279-
}
248+
// Create onboarding URL AFTER git operations so getVcsRemoteInfo() can detect the repo
249+
let connectUrl: string | undefined;
250+
let nxCloudInfo: string | undefined;
280251

281-
// Template flow: cloud URL generation and completion message
282-
if (isTemplate) {
283-
if (nxCloud !== 'skip' && nxCloud !== 'never') {
284-
const aiModeForCloud = isAiAgent();
285-
if (aiModeForCloud) {
286-
logProgress('configuring', 'Configuring Nx Cloud...');
287-
}
288-
const token = options.skipCloudConnect
289-
? undefined
290-
: readNxCloudToken(directory);
252+
if (nxCloud !== 'skip' && nxCloud !== 'never') {
253+
// "Yes" or "Maybe later" — generate URL, update README, show banner
254+
const aiModeForCloud = isAiAgent();
255+
if (aiModeForCloud) {
256+
logProgress('configuring', 'Configuring Nx Cloud...');
257+
}
258+
// skipCloudConnect=true (Maybe later): Skip readNxCloudToken() since no token exists
259+
// skipCloudConnect=false (Yes): Read the token as before (cloud was connected)
260+
const token = options.skipCloudConnect
261+
? undefined
262+
: readNxCloudToken(directory);
291263

292-
connectUrl = await createNxCloudOnboardingUrl(
293-
nxCloud,
294-
token,
295-
directory,
296-
useGitHub
297-
);
264+
connectUrl = await createNxCloudOnboardingUrl(
265+
nxCloud,
266+
token,
267+
directory,
268+
useGitHub
269+
);
298270

299-
cloudConnectUrl = connectUrl;
271+
// Store for SIGINT handler
272+
cloudConnectUrl = connectUrl;
300273

274+
// Update README with connect URL (strips markers, adds connect section)
275+
// Then commit the change - amend if not pushed, new commit if already pushed
276+
if (isTemplate) {
301277
const readmeUpdated = addConnectUrlToReadme(directory, connectUrl);
302278
if (readmeUpdated && !skipGit && commit) {
303279
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
304280
await amendOrCommitReadme(directory, alreadyPushed);
305281
}
282+
}
306283

307-
nxCloudInfo = await getNxCloudInfo(nxCloud, connectUrl, pushedToVcs);
308-
} else {
309-
// Strip marker comments from README
310-
const readmeUpdated = addConnectUrlToReadme(directory, undefined);
311-
if (readmeUpdated && !skipGit && commit) {
312-
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
313-
await amendOrCommitReadme(directory, alreadyPushed);
314-
}
284+
nxCloudInfo = await getNxCloudInfo(
285+
connectUrl,
286+
pushedToVcs,
287+
options.completionMessageKey,
288+
name
289+
);
290+
} else if (isTemplate && (nxCloud === 'skip' || nxCloud === 'never')) {
291+
// Strip marker comments from README
292+
const readmeUpdated = addConnectUrlToReadme(directory, undefined);
293+
if (readmeUpdated && !skipGit && commit) {
294+
const alreadyPushed = pushedToVcs === VcsPushStatus.PushedToVcs;
295+
await amendOrCommitReadme(directory, alreadyPushed);
296+
}
315297

316-
if (nxCloud === 'skip') {
317-
nxCloudInfo = getSkippedNxCloudInfo();
318-
}
298+
// Only show "nx connect" message for 'skip', not 'never'
299+
if (nxCloud === 'skip') {
300+
nxCloudInfo = getSkippedNxCloudInfo();
319301
}
320302
}
321303

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,48 @@ export async function determineTemplate(
107107
}>
108108
): Promise<string | 'custom'> {
109109
if (parsedArgs.template) return parsedArgs.template;
110-
return 'custom';
110+
if (parsedArgs.preset) return 'custom';
111+
if (!parsedArgs.interactive || isCI()) return 'custom';
112+
// Docs generation needs preset flow to document all presets
113+
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true') return 'custom';
114+
// Template flow requires git for cloning - fall back to custom preset if git is not available
115+
if (!isGitAvailable()) return 'custom';
116+
const { template } = await enquirer.prompt<{ template: string }>([
117+
{
118+
name: 'template',
119+
message: 'Which starter do you want to use?',
120+
type: 'autocomplete',
121+
choices: [
122+
{
123+
name: 'nrwl/empty-template',
124+
message: 'Minimal (empty monorepo without projects)',
125+
},
126+
{
127+
name: 'nrwl/react-template',
128+
message:
129+
'React (fullstack monorepo with React and Express)',
130+
},
131+
{
132+
name: 'nrwl/angular-template',
133+
message:
134+
'Angular (fullstack monorepo with Angular and Express)',
135+
},
136+
{
137+
name: 'nrwl/typescript-template',
138+
message:
139+
'NPM Packages (monorepo with TypeScript packages ready to publish)',
140+
},
141+
{
142+
name: 'custom',
143+
message:
144+
'Custom (advanced setup with additional frameworks)',
145+
},
146+
],
147+
initial: 0,
148+
},
149+
]);
150+
151+
return template;
111152
}
112153

113154
export async function determineAiAgents(

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,8 @@ describe('ab-testing', () => {
9999
).toBe('0');
100100
});
101101

102-
// NXC-4020: Locked to variant 0 to match v22.1.3
103-
it('should return 0 for standard URLs (NXC-4020)', () => {
104-
expect(getBannerVariant('https://cloud.nx.app/connect/abc')).toBe('0');
102+
it('should return 2 for standard URLs', () => {
103+
expect(getBannerVariant('https://cloud.nx.app/connect/abc')).toBe('2');
105104
});
106105
});
107106

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,18 @@ export function isEnterpriseCloudUrl(cloudUrl?: string): boolean {
142142
* @param cloudUrl - The Nx Cloud URL. If enterprise, always returns '0'.
143143
*/
144144
export function getBannerVariant(cloudUrl?: string): BannerVariant {
145-
return '0';
145+
// Enterprise URLs always get plain link (variant 0)
146+
if (isEnterpriseCloudUrl(cloudUrl)) {
147+
return '0';
148+
}
149+
150+
// Docs generation uses variant 0 for deterministic output
151+
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true') {
152+
return '0';
153+
}
154+
155+
// Standard URLs get variant 2 banner
156+
return '2';
146157
}
147158

148159
export const NxCloudChoices = [
@@ -181,22 +192,21 @@ const messageOptions: Record<string, MessageData[]> = {
181192
],
182193
/**
183194
* These messages are a fallback for setting up CI as well as when migrating major versions
184-
* NXC-4020: Restored to v22.1.3 wording
195+
* Locked to "full platform" messaging (CLOUD-4235)
185196
*/
186197
setupNxCloud: [
187198
{
188-
code: 'enable-caching2',
189-
message: 'Would you like remote caching to make your build faster?',
199+
code: 'cloud-v2-full-platform-visit',
200+
message: 'Try the full Nx platform?',
190201
initial: 0,
191202
choices: [
192203
{ value: 'yes', name: 'Yes' },
193-
{ value: 'skip', name: 'No - I would not like remote caching' },
204+
{ value: 'skip', name: 'Skip' },
194205
],
195206
footer:
196-
'\nRead more about remote caching at https://nx.dev/ci/features/remote-cache',
197-
hint: '(can be disabled any time)',
207+
'\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud',
198208
fallback: undefined,
199-
completionMessage: 'cache-setup',
209+
completionMessage: 'platform-setup',
200210
},
201211
],
202212
/**

0 commit comments

Comments
 (0)