From 1d3e6a35bdecc01693ffa4c18dfc6feaceceb68f Mon Sep 17 00:00:00 2001 From: Harshi-Shah-CS Date: Mon, 2 Nov 2026 12:24:04 +0530 Subject: [PATCH 1/2] fix(functions): drop the -p short flag from launch:functions:serve --port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V2 keeps -p for --password on launch:env:update only, so the port is now set with --port alone. A V1-style `launch:functions:serve -p 4000` stops with "Nonexistent flag: -p" and exit 2. The PORT environment variable still wins over --port, as before. The examples and the README usage block now show --port. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .talismanrc | 2 +- README.md | 6 +++--- src/commands/launch/functions/serve.test.ts | 7 ++++--- src/commands/launch/functions/serve.ts | 2 +- src/functions/function.inputs.ts | 1 - test/integration/functions-serve-port.test.ts | 7 +++++++ 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.talismanrc b/.talismanrc index 9fa05033..480ef9a8 100644 --- a/.talismanrc +++ b/.talismanrc @@ -332,7 +332,7 @@ fileignoreconfig: - filename: src/commands/launch/env/update.test.ts checksum: 2da219594e7bdd589adc0eff9ff035252a75aaa51dfa20514b18fd3266d7b752 - filename: README.md - checksum: b6af89e3dea468d56b666f4098a9b2224a5224ca45c9e94f7970d9e6674f3e6e + checksum: 916cd3b37e9c40a5d7d6d38437e279bbc573791588d4729ac741309134187724 allowed_patterns: - "password-protection" - "-p, --password" diff --git a/README.md b/README.md index a9f9ae72..ebe2cc8e 100644 --- a/README.md +++ b/README.md @@ -518,11 +518,11 @@ Serve cloud functions ``` USAGE - $ csdx launch:functions:serve [-p ] [-d ] + $ csdx launch:functions:serve [--port ] [-d ] FLAGS -d, --data-dir= Current working directory - -p, --port= [default: 3000] Port number + --port= [default: 3000] Port number DESCRIPTION Serve cloud functions @@ -534,7 +534,7 @@ EXAMPLES $ csdx launch:functions:serve --data-dir - $ csdx launch:functions:serve --data-dir -p + $ csdx launch:functions:serve --data-dir --port ``` _See code: [src/commands/launch/functions/serve.ts](https://github.com/contentstack/launch-cli/blob/v2.0.0-beta.0/src/commands/launch/functions/serve.ts)_ diff --git a/src/commands/launch/functions/serve.test.ts b/src/commands/launch/functions/serve.test.ts index 4663584d..2738818f 100644 --- a/src/commands/launch/functions/serve.test.ts +++ b/src/commands/launch/functions/serve.test.ts @@ -108,12 +108,13 @@ describe('launch:functions:serve flag definitions', () => { '$ csdx launch:functions:serve', '$ csdx launch:functions:serve --port ', '$ csdx launch:functions:serve --data-dir ', - '$ csdx launch:functions:serve --data-dir -p ', + '$ csdx launch:functions:serve --data-dir --port ', ]); }); - it('declares a port flag defaulting to 3000 and a data-dir flag', () => { - expect(Functions.flags.port).toMatchObject({ char: 'p', default: '3000', description: 'Port number' }); + it('declares a port flag defaulting to 3000 with no short flag, and a data-dir flag', () => { + expect(Functions.flags.port).toMatchObject({ default: '3000', description: 'Port number' }); + expect(Functions.flags.port.char).toBeUndefined(); expect(Functions.flags.port.env).toBeUndefined(); expect(Functions.flags['data-dir']).toMatchObject({ char: 'd', description: 'Current working directory' }); expect(Functions.flags['data-dir'].default).toBeUndefined(); diff --git a/src/commands/launch/functions/serve.ts b/src/commands/launch/functions/serve.ts index d01c018b..2eaf8343 100755 --- a/src/commands/launch/functions/serve.ts +++ b/src/commands/launch/functions/serve.ts @@ -15,7 +15,7 @@ export default class Functions extends Command { '$ csdx launch:functions:serve', '$ csdx launch:functions:serve --port ', '$ csdx launch:functions:serve --data-dir ', - '$ csdx launch:functions:serve --data-dir -p ', + '$ csdx launch:functions:serve --data-dir --port ', ]; static flags = serveFlags; diff --git a/src/functions/function.inputs.ts b/src/functions/function.inputs.ts index a0fb6434..7d808882 100644 --- a/src/functions/function.inputs.ts +++ b/src/functions/function.inputs.ts @@ -6,7 +6,6 @@ export const DEFAULT_SERVE_PORT = '3000'; export const serveFlags = { port: Flags.string({ - char: 'p', default: DEFAULT_SERVE_PORT, description: 'Port number', }), diff --git a/test/integration/functions-serve-port.test.ts b/test/integration/functions-serve-port.test.ts index 4436382b..09ecd557 100644 --- a/test/integration/functions-serve-port.test.ts +++ b/test/integration/functions-serve-port.test.ts @@ -89,4 +89,11 @@ describe('integration: launch:functions:serve port handling', () => { expect(stderr).toBe(''); await expect(errorLog()).resolves.toBe(INVALID_PORT_RECORD); }); + + it('rejects V1 -p as a nonexistent flag, so the port is only ever set with --port', async () => { + const { error } = await runCommand(['launch:functions:serve', '-p', '4000', '--data-dir', projectDir], config); + + expect(error?.message).toContain('Nonexistent flag: -p'); + expect(error?.oclif?.exit).toBe(2); + }); }); From 6b66ac2f4643df1b6e6804467cd010ad6a1cdb66 Mon Sep 17 00:00:00 2001 From: Harshi-Shah-CS Date: Mon, 2 Nov 2026 12:24:49 +0530 Subject: [PATCH 2/2] fix(env): refuse a password typed after --password without echoing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --password on launch:env:update takes no value: the password comes from the hidden prompt or, without a terminal, from standard input. A value typed after it (-p , --password , --password= or -p) used to reach oclif, which printed it back as "Unexpected argument: " or "Nonexistent flag: -", putting the password in the terminal or CI log. LaunchCommand now refuses those forms before parsing, on commands that declare --password, with exit 2 and a message that never repeats the value. -p on its own, last, or before another flag still reads the password as before. Commands without --password leave -p to the flag parser. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .talismanrc | 10 ++++-- src/core/launch-command.test.ts | 10 ++++++ src/core/launch-command.ts | 5 +++ src/core/password-argv.test.ts | 38 ++++++++++++++++++++ src/core/password-argv.ts | 20 +++++++++++ test/integration/environments-update.test.ts | 13 +++++-- 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/core/password-argv.test.ts create mode 100644 src/core/password-argv.ts diff --git a/.talismanrc b/.talismanrc index 480ef9a8..5269c43b 100644 --- a/.talismanrc +++ b/.talismanrc @@ -268,9 +268,9 @@ fileignoreconfig: - filename: test/integration/projects-delete.test.ts checksum: bb59cbe1f4f7b3ac6aa7944067c1ab1214a7c5d20229e9d6577cdaabec56622e - filename: src/core/launch-command.ts - checksum: 01904855aca54cbbcb846069cee5a0732da1db082110e7e454a5c1c532ab02fb + checksum: 999468661532665870b81face9086998e543df648175e2417045840dc97cb1dd - filename: src/core/launch-command.test.ts - checksum: c4d3785f25377db560bb9daa5d0d080a0131056e6c31e3b45cb4f1f2347305ed + checksum: f2570ccfd27539240b251afafd67d5525e5831c4144daad7f7b909eb25e7ee0f - filename: src/projects/project.inputs.test.ts checksum: 8242f4d40388f1a7f1d08c11d2571cb45a9294069a8248a5272bc5606807bb60 - filename: test/integration/projects-command-flows.test.ts @@ -326,7 +326,7 @@ fileignoreconfig: - filename: src/core/prompt.test.ts checksum: 7be5497acf4cee870667751d44aa230e309784df76dc9e95d5ec2c4e6e6a57e0 - filename: test/integration/environments-update.test.ts - checksum: 4cd0106bf1881d373d23f063314627057d85cab0c43b793f013ffc2ca8859036 + checksum: 249f27ad679b68f40eac3af90c616229838bceffad57ba508dd298ab053fa51a - filename: src/commands/launch/env/update.ts checksum: 06add97108d878eaf415e32160439b8963683e63a0e37ce77a56f66b88383c7a - filename: src/commands/launch/env/update.test.ts @@ -517,4 +517,8 @@ fileignoreconfig: checksum: 44ff11b9b4f3d7583d5faa4a2df45518c959eb4c29896cafe05ead19f215e4c7 - filename: src/commands/launch/secrets/unset.test.ts checksum: a00e503cc66876fa1ef37eaab1a91e2e013af44d3add8ecd6fa08f269d550002 +- filename: src/core/password-argv.ts + checksum: 22a5aa7d95c6a5113a034d2cc9af4f152f8a366f1c271f86cd317ea2046f5b1d +- filename: src/core/password-argv.test.ts + checksum: df87a19d09ff07b95d8118dd7e6bae5a13a86f0ecb8dce97e6022481d1e11256 version: "1.0" diff --git a/src/core/launch-command.test.ts b/src/core/launch-command.test.ts index 390e0a4c..17c19ac7 100644 --- a/src/core/launch-command.test.ts +++ b/src/core/launch-command.test.ts @@ -461,6 +461,16 @@ describe('LaunchCommand.init authentication gate', () => { }); }); +describe('LaunchCommand.init password on the command line', () => { + it('leaves -p and its value to the flag parser when the command has no --password flag', async () => { + const instance = new Probe(['-p', 'staging'], {} as never); + const parserRefusal = new Error('Nonexistent flag: -p'); + (instance as unknown as { parse: jest.Mock }).parse = jest.fn().mockRejectedValue(parserRefusal); + + await expect(instance.init()).rejects.toBe(parserRefusal); + }); +}); + describe('LaunchCommand.init terminal detection', () => { it.each([[false], [undefined]])('resolves isTTY to false when process.stdin.isTTY is %p', async (isTTY) => { const instance = probe(); diff --git a/src/core/launch-command.ts b/src/core/launch-command.ts index 15aeb3a0..46b518f4 100644 --- a/src/core/launch-command.ts +++ b/src/core/launch-command.ts @@ -15,6 +15,7 @@ import type { AnyInputs, Resolved } from './inputs'; import { resolveInputsTraced } from './resolve'; import type { Rule } from './rules'; import { cancelOnInterrupt } from './interruptible-ux'; +import { refusePasswordOnArgv } from './password-argv'; import type { UxLike } from './prompt'; import { registerSearchList } from './search-list'; import type { FailureOutput } from './failure'; @@ -119,6 +120,10 @@ export abstract class LaunchCommand extends Com registerSearchList(); process.stdin.setMaxListeners(STDIN_MAX_LISTENERS); + if ('password' in this.contract.flags) { + refusePasswordOnArgv(this.argv); + } + const { flags } = await this.parse({ flags: this.contract.flags, baseFlags: LaunchCommand.baseFlags, diff --git a/src/core/password-argv.test.ts b/src/core/password-argv.test.ts new file mode 100644 index 00000000..d39057ab --- /dev/null +++ b/src/core/password-argv.test.ts @@ -0,0 +1,38 @@ +import { randomBytes } from 'node:crypto'; + +import { UsageError } from './errors'; +import { PASSWORD_ON_ARGV, refusePasswordOnArgv } from './password-argv'; + +const password = randomBytes(8).toString('hex'); + +describe('refusePasswordOnArgv', () => { + it.each([ + ['-p followed by a value', ['--env', 'staging', '-p', password]], + ['--password followed by a value', ['--password', password, '--env', 'staging']], + ['--password=', [`--password=${password}`]], + ['-p written as one word', [`-p${password}`]], + ])('refuses %s with a usage error that never repeats the value', (_form, argv) => { + const refusal = captured(() => refusePasswordOnArgv(argv)); + + expect(refusal).toBeInstanceOf(UsageError); + expect(refusal.message).toBe(PASSWORD_ON_ARGV); + }); + + it.each([ + ['-p last', ['--password-protection', 'enable', '-u', 'admin', '-p']], + ['-p before another flag', ['-p', '--yes']], + ['--password before another flag', ['--password', '-u', 'admin']], + ])('lets %s through, so the password can come from the prompt or standard input', (_form, argv) => { + expect(() => refusePasswordOnArgv(argv)).not.toThrow(); + }); +}); + +function captured(act: () => void): Error { + try { + act(); + } catch (error) { + return error as Error; + } + + throw new Error('expected a refusal'); +} diff --git a/src/core/password-argv.ts b/src/core/password-argv.ts new file mode 100644 index 00000000..04861086 --- /dev/null +++ b/src/core/password-argv.ts @@ -0,0 +1,20 @@ +import { UsageError } from './errors'; + +const PASSWORD_SWITCHES = ['-p', '--password']; + +export const PASSWORD_ON_ARGV = + '--password takes no value. Enter the password at the hidden prompt, or pipe it on standard input.'; + +export function refusePasswordOnArgv(argv: readonly string[]): void { + if (argv.some((arg, index) => carriesPassword(arg, argv[index + 1]))) { + throw new UsageError(PASSWORD_ON_ARGV); + } +} + +function carriesPassword(arg: string, next: string | undefined): boolean { + if (PASSWORD_SWITCHES.includes(arg)) { + return next !== undefined && !next.startsWith('-'); + } + + return arg.startsWith('--password=') || arg.startsWith('-p'); +} diff --git a/test/integration/environments-update.test.ts b/test/integration/environments-update.test.ts index a23dd3c9..80a28511 100644 --- a/test/integration/environments-update.test.ts +++ b/test/integration/environments-update.test.ts @@ -107,10 +107,19 @@ describe('integration: launch:env:update on the wire', () => { expect(onWire).toEqual([]); }); - it('refuses a password value on the command line with exit 2 before any request', async () => { - const { error } = await runCommand(updateArgs('--password-protection', 'enable', '-p', 'on-argv'), config); + it('refuses a password value on the command line with exit 2 before any request, without echoing it', async () => { + const password = randomBytes(8).toString('hex'); + + const { error, stdout, stderr } = await runCommand( + updateArgs('--password-protection', 'enable', '-p', password), + config, + ); expect(error?.oclif?.exit).toBe(2); + expect(error?.message).toBe( + '--password takes no value. Enter the password at the hidden prompt, or pipe it on standard input.', + ); + expect(`${stdout}${stderr}`).not.toContain(password); expect(onWire).toEqual([]); });