Skip to content

Commit f943170

Browse files
feat(projects): ask create's project type as "Choose a project type to proceed"
The project-type picker in launch:projects:create now reads "Choose a project type to proceed" with the choices "Continue with GitHub" and "Continue with FileUpload", in place of "Project type" / "GitHub" / "FileUpload". Only the labels change: each choice still answers the bare GitHub or FileUpload, so --type and the rest of create are unchanged. The question is held in PROJECT_TYPE_QUESTION. Also raise the stdin listener limit to 50 (STDIN_MAX_LISTENERS) in LaunchCommand.init(). The inquirer bundled with cli-utilities adds a keypress listener to stdin per prompt, so a run of more than ten prompts - projects:create reaches that - printed a MaxListenersExceededWarning. The stdin guard test allows exactly that line. Talisman flags secret-free lines in project.inputs.test.ts - lines containing "key" or "pass". .talismanrc allows exactly those lines, narrowed per file like the existing entries. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e06460f commit f943170

9 files changed

Lines changed: 44 additions & 18 deletions

‎.talismanrc‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,10 @@ fileignoreconfig:
166166
- "key === 'authorisationType'"
167167
- "Pass exactly one of --org, --limit"
168168
- "passes the declared rules through to the resolver"
169+
- filename: src/projects/project.inputs.test.ts
170+
allowed_patterns:
171+
- "Object\\.keys\\(projectFlags\\)"
172+
- "projectFlags\\[flag as keyof typeof projectFlags\\]"
173+
- "passes a name at the limit through unchanged"
174+
- "passes a description at the limit through unchanged"
169175
version: "1.0"

‎src/core/constants.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export const DEFAULT_LIMIT = 100;
88
export const CLIENT_MAX_LIMIT = 1000;
99
export const PICKER_PAGE_SIZE = 100;
1010
export const MAX_PAGES = 100;
11+
export const STDIN_MAX_LISTENERS = 50;

‎src/core/launch-command.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,21 @@ describe('LaunchCommand.init', () => {
333333
authSpy.mockRestore();
334334
});
335335

336+
it('raises the stdin listener limit so a long run of prompts does not warn of a leak', async () => {
337+
const instance = probe();
338+
const authSpy = jest.spyOn(authHandler, 'isAuthenticated').mockReturnValue(true);
339+
Object.defineProperty(instance, 'launchRegion', { value: { launchHubUrl: 'https://launch-api.test' }, configurable: true });
340+
Object.defineProperty(instance, 'config', { value: { userAgent: 'cli/2.0.0' }, configurable: true });
341+
(instance as unknown as { parse: jest.Mock }).parse = jest.fn().mockResolvedValue({ flags: {} });
342+
const previous = process.stdin.getMaxListeners();
343+
344+
await instance.init();
345+
346+
expect(process.stdin.getMaxListeners()).toBe(50);
347+
process.stdin.setMaxListeners(previous);
348+
authSpy.mockRestore();
349+
});
350+
336351
it('derives the hub url from the configured region cma when the region declares no launch hub url', async () => {
337352
const instance = probe();
338353
const authSpy = jest.spyOn(authHandler, 'isAuthenticated').mockReturnValue(true);

‎src/core/launch-command.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve as resolvePath } from 'node:path';
33
import { Command } from '@contentstack/cli-command';
44
import { cliux, configHandler, isAuthenticated } from '@contentstack/cli-utilities';
55

6-
import { EXIT_RUNTIME, PROJECT_CONFIG_FILE } from './constants';
6+
import { EXIT_RUNTIME, PROJECT_CONFIG_FILE, STDIN_MAX_LISTENERS } from './constants';
77
import { ProjectConfig, ProjectConfigStore } from './project-config';
88
import { RegionLike, resolveLaunchHubUrl } from './region';
99
import { LaunchError, UsageError } from './errors';
@@ -102,6 +102,7 @@ export abstract class LaunchCommand<S extends AnyInputs = AnyInputs> extends Com
102102
async init(): Promise<void> {
103103
await super.init();
104104
registerSearchList();
105+
process.stdin.setMaxListeners(STDIN_MAX_LISTENERS);
105106

106107
const { flags } = await this.parse({
107108
flags: this.contract.flags,

‎src/core/tty-streams.guard.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Occurrence, SourceFile, describeOccurrence, productionSources } from '.
22

33
const PLUMBING_FILES = ['core/launch-command.ts', 'core/service-context.ts', 'core/resolution.ts'];
44
const PLUMBING_LINE = /^isTTY\??:\s*(boolean|args\.isTTY|options\.isTTY|Boolean\(process\.stdin\.isTTY\)),?;?$/;
5+
const LISTENER_LIMIT_LINE = /^process\.stdin\.setMaxListeners\(STDIN_MAX_LISTENERS\);$/;
56
const PROMPT_DECISION = /\.inquire\b|\bask[A-Z]\w*\(|\bprompt\b|\bthrow new (MissingInputError|UsageError)\b/;
67
const GATE_WINDOW = 4;
78

@@ -21,7 +22,7 @@ function stdinReads(source: SourceFile): Occurrence[] {
2122
}
2223

2324
function isPlumbing(read: Occurrence): boolean {
24-
return PLUMBING_FILES.includes(read.path) && PLUMBING_LINE.test(read.text);
25+
return PLUMBING_FILES.includes(read.path) && (PLUMBING_LINE.test(read.text) || LISTENER_LIMIT_LINE.test(read.text));
2526
}
2627

2728
function isPromptDecision(source: SourceFile, read: Occurrence): boolean {

‎src/projects/project.create.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ describe('ProjectCreator prompting order and refusals', () => {
518518
await creator.create({ org: ORG, dataDir, configPath: configPathIn(dataDir) });
519519

520520
expect(asked).toEqual([
521-
'Project type',
521+
'Choose a project type to proceed',
522522
'Project name',
523523
'Environment name',
524524
'Choose a branch',
@@ -554,7 +554,7 @@ describe('ProjectCreator prompting order and refusals', () => {
554554
await creator.create({ org: ORG, dataDir, configPath: configPathIn(dataDir) });
555555

556556
expect(uploadArchive).toHaveBeenCalledTimes(1);
557-
expect(askedBeforeUpload).toEqual(['Project type']);
557+
expect(askedBeforeUpload).toEqual(['Choose a project type to proceed']);
558558
expect(asked.slice(1, 4)).toEqual(['Project name', 'Environment name', 'Framework preset']);
559559
});
560560

@@ -788,7 +788,7 @@ describe('ProjectCreator on the FileUpload path', () => {
788788
expect((failure as Error).message).toContain('over the 100 MB Launch accepts for a file upload');
789789
expect(calls.signedUploadUrl).toEqual([]);
790790
expect(uploadArchive).not.toHaveBeenCalled();
791-
expect(asked).toEqual(['Project type']);
791+
expect(asked).toEqual(['Choose a project type to proceed']);
792792
});
793793

794794
it('leaves a --config file that lives inside the data dir out of the upload', async () => {

‎src/projects/project.inputs.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ describe('the --type flag', () => {
117117
{
118118
type: 'search-list',
119119
name: 'value',
120-
message: 'Project type',
120+
message: 'Choose a project type to proceed',
121121
choices: [
122-
{ name: 'GitHub', value: 'GitHub' },
123-
{ name: 'FileUpload', value: 'FileUpload' },
122+
{ name: 'Continue with GitHub', value: 'GitHub' },
123+
{ name: 'Continue with FileUpload', value: 'FileUpload' },
124124
],
125125
default: undefined,
126126
},
@@ -140,6 +140,6 @@ describe('the --type flag', () => {
140140
await expect(
141141
projectResolution.type.prompt({ services: { api: {} as ApiSurface, ux, isTTY: true }, resolved: {} }),
142142
).resolves.toBe('GitHub');
143-
expect(asked).toEqual(['Project type']);
143+
expect(asked).toEqual(['Choose a project type to proceed']);
144144
});
145145
});

‎src/projects/project.inputs.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ export function projectTypeChoiceOf(value: string): ProjectTypeChoice {
2626
return oneOf('type', value, PROJECT_TYPE_CHOICES);
2727
}
2828

29+
export const PROJECT_TYPE_QUESTION = 'Choose a project type to proceed';
30+
2931
export function askProjectType(ux: UxLike): Promise<string> {
3032
return askChoice(
3133
ux,
32-
'Project type',
33-
PROJECT_TYPE_CHOICES.map((value) => ({ name: value, value })),
34+
PROJECT_TYPE_QUESTION,
35+
PROJECT_TYPE_CHOICES.map((value) => ({ name: `Continue with ${value}`, value })),
3436
);
3537
}
3638

‎test/integration/projects-create.test.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ describe('integration: launch:projects:create on the wire', () => {
467467

468468
it('asks on a terminal in the pinned order: type, organization, project name, environment name, then the build and Contentstack Authentication', async () => {
469469
const prompts = answerPrompts({
470-
'Project type': 'FileUpload',
470+
'Choose a project type to proceed': 'FileUpload',
471471
'Choose an organization': ORG_UID,
472472
'Project name': 'My Site',
473473
'Environment name': 'Default',
@@ -503,7 +503,7 @@ describe('integration: launch:projects:create on the wire', () => {
503503
expect(organizations.isDone()).toBe(true);
504504
expect(create.isDone()).toBe(true);
505505
expect(prompts.messages).toEqual([
506-
'Project type',
506+
'Choose a project type to proceed',
507507
'Choose an organization',
508508
'Project name',
509509
'Environment name',
@@ -518,7 +518,7 @@ describe('integration: launch:projects:create on the wire', () => {
518518

519519
it('exits 3 as cancelled, not 130, and creates nothing when Ctrl-C is pressed at the last create prompt', async () => {
520520
const prompts = answerPrompts({
521-
'Project type': 'FileUpload',
521+
'Choose a project type to proceed': 'FileUpload',
522522
'Choose an organization': ORG_UID,
523523
'Project name': 'My Site',
524524
'Environment name': 'Default',
@@ -556,7 +556,7 @@ describe('integration: launch:projects:create on the wire', () => {
556556
it('asks all ten GitHub prompts in the pinned order and submits exactly what was answered', async () => {
557557
let body: unknown;
558558
const prompts = answerPrompts({
559-
'Project type': 'GitHub',
559+
'Choose a project type to proceed': 'GitHub',
560560
'Choose an organization': ORG_UID,
561561
'Project name': 'My Site',
562562
'Environment name': 'Default',
@@ -594,7 +594,7 @@ describe('integration: launch:projects:create on the wire', () => {
594594
expect(error).toBeUndefined();
595595
expect([branches.isDone(), create.isDone()]).toEqual([true, true]);
596596
expect(prompts.messages).toEqual([
597-
'Project type',
597+
'Choose a project type to proceed',
598598
'Choose an organization',
599599
'Project name',
600600
'Environment name',
@@ -644,7 +644,7 @@ describe('integration: launch:projects:create on the wire', () => {
644644
cloneOf('my-org/my-repo');
645645
let body: unknown;
646646
const prompts = answerPrompts({
647-
'Project type': 'GitHub',
647+
'Choose a project type to proceed': 'GitHub',
648648
'Project name': 'My Site',
649649
'Environment name': 'Default',
650650
'Choose a branch': 'main',
@@ -675,7 +675,7 @@ describe('integration: launch:projects:create on the wire', () => {
675675

676676
expect(error).toBeUndefined();
677677
expect(prompts.messages).toEqual([
678-
'Project type',
678+
'Choose a project type to proceed',
679679
'Project name',
680680
'Environment name',
681681
'Choose a branch',

0 commit comments

Comments
 (0)