Skip to content

Commit 5b9b3fa

Browse files
committed
fix(cli): quote a profile name a pasted command would otherwise split
The suggestion configure prints is meant to be pasted, and it interpolated the profile name bare. Profile-name validation is creation-only by design — the validator says so, because a hand-written `[profile my stack]` has to keep resolving — so a name carrying whitespace, or a `;` that would end the pasted command and start another, reaches this message unchecked. Names that already satisfy the creation rule stay bare; the rest are single quoted, embedded quotes included. Redaction runs first, so a control character becomes a space and is then quoted rather than splitting the command.
1 parent 85d4308 commit 5b9b3fa

2 files changed

Lines changed: 54 additions & 4 deletions

File tree

packages/sim-cli/src/commands/configure.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,38 @@ describe('configure and the root globals', () => {
232232
)
233233
})
234234

235-
/** The profile name is caller-supplied, so it is redacted like the value. */
235+
/**
236+
* The profile name is caller-supplied, so it is redacted like the value —
237+
* and redaction turns the separator into a space, which the suggestion then
238+
* has to quote to stay one argument.
239+
*/
236240
it('redacts a control character out of the profile it suggests', async () => {
237241
await expect(run('-P', 'dev\u2028sim login', '--output', 'json')).rejects.toThrow(
238-
'sim configure --profile dev sim login --set-output json'
242+
"sim configure --profile 'dev sim login' --set-output json"
243+
)
244+
})
245+
246+
/**
247+
* Profile-name validation is creation-only by design, so a hand-written
248+
* `[profile my stack]` keeps resolving and reaches this suggestion. Unquoted,
249+
* a name carrying a `;` would end the pasted command and start another.
250+
*/
251+
it('quotes a profile name a pasted command would otherwise split', async () => {
252+
await expect(run('-P', 'my stack', '--output', 'json')).rejects.toThrow(
253+
"sim configure --profile 'my stack' --set-output json"
254+
)
255+
await expect(run('-P', 'a;rm -rf x', '--output', 'json')).rejects.toThrow(
256+
"sim configure --profile 'a;rm -rf x' --set-output json"
257+
)
258+
await expect(run('-P', "it's mine", '--output', 'json')).rejects.toThrow(
259+
"sim configure --profile 'it'\\''s mine' --set-output json"
260+
)
261+
})
262+
263+
/** A name that already satisfies the creation rule needs no quoting noise. */
264+
it('leaves an ordinary profile name bare', async () => {
265+
await expect(run('-P', 'dev.2_a-b', '--output', 'json')).rejects.toThrow(
266+
'sim configure --profile dev.2_a-b --set-output json'
239267
)
240268
})
241269

packages/sim-cli/src/commands/configure.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import {
77
resolveAuthenticationProfileName,
88
writeConfigProfile,
99
} from '../config/index'
10-
import { normalizeEndpoint, normalizeWorkspaceId, redact } from '../config/profile'
10+
import {
11+
normalizeEndpoint,
12+
normalizeWorkspaceId,
13+
PROFILE_NAME_PATTERN,
14+
redact,
15+
} from '../config/profile'
1116
import { globalsOf, profileFrom } from '../context'
1217
import { SimApiError } from '../http/client'
1318

@@ -52,6 +57,21 @@ function requireValue(value: string | undefined, flag: string, key: string): voi
5257
* they arrive through `sim login`, which is the only path that mints a key with
5358
* a recorded consent behind it.
5459
*/
60+
/**
61+
* Quotes a profile name that a pasted command would otherwise split.
62+
*
63+
* Profile-name validation is creation-only by design, so a hand-written
64+
* `[profile my stack]` keeps resolving — and reaches this suggestion carrying
65+
* whitespace, or a `;` that would end the pasted command and start another.
66+
* Names that already match the creation rule are left bare, since quoting every
67+
* one of them would only add noise to the common case.
68+
*/
69+
function quoteProfileArgument(name: string): string {
70+
const redacted = redact(name)
71+
if (PROFILE_NAME_PATTERN.test(redacted)) return redacted
72+
return `'${redacted.replaceAll("'", "'\\''")}'`
73+
}
74+
5575
export function configureCommand(): Command {
5676
return new Command('configure')
5777
.description("Set a profile's endpoint, default workspace, or output format")
@@ -71,7 +91,9 @@ export function configureCommand(): Command {
7191
) => {
7292
const globals = globalsOf(command)
7393
const selectedProfile = globals.profile || process.env.SIM_PROFILE
74-
const profileArg = selectedProfile ? ` --profile ${redact(selectedProfile)}` : ''
94+
const profileArg = selectedProfile
95+
? ` --profile ${quoteProfileArgument(selectedProfile)}`
96+
: ''
7597
for (const { option, flag, setFlag } of GLOBAL_FLAG_TWINS) {
7698
const value = globals[option]
7799
if (value === undefined) continue

0 commit comments

Comments
 (0)