Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/__tests__/env-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,26 @@ scope: 'user',
]);

expect(content).toBe(
'export API_URL="https://example.com"\nexport TOKEN="abc123"\n',
"export API_URL='https://example.com'\nexport TOKEN='abc123'\n",
);
});

it('should shell-quote values containing shell metacharacters', () => {
// Values flow from the team repo's env/env.yaml into env.sh, which every
// member sources from their shell profile. Quotes / `$` / backticks must
// be taken literally and must not break or inject into the sourced shell.
const content = handler.generateEnvFile([
{ key: 'CONN', value: 'a"b$c' },
{ key: 'GREETING', value: "it's" },
]);

expect(content).toBe(
"export CONN='a\"b$c'\nexport GREETING='it'\\''s'\n",
);
// No raw double-quote wrapping that the old code produced.
expect(content).not.toContain('="a');
});

it('should return just a newline for empty variables', () => {
const content = handler.generateEnvFile([]);
expect(content).toBe('\n');
Expand Down Expand Up @@ -257,8 +273,8 @@ scope: 'user',
const envShPath = path.join(homeDir, '.teamai', 'env.sh');
expect(await fse.pathExists(envShPath)).toBe(true);
const content = await fse.readFile(envShPath, 'utf-8');
expect(content).toContain('export TGIT_API_BASE="https://git.woa.com/api/v3"');
expect(content).toContain('export MODEL_ENDPOINT="https://api.example.com"');
expect(content).toContain("export TGIT_API_BASE='https://git.woa.com/api/v3'");
expect(content).toContain("export MODEL_ENDPOINT='https://api.example.com'");
});

it('should inject source line into shell profile (bash)', async () => {
Expand Down
18 changes: 17 additions & 1 deletion src/resources/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ const EnvYamlSchema = z.object({
export type EnvVariable = z.infer<typeof EnvVariableSchema>;
export type EnvYaml = z.infer<typeof EnvYamlSchema>;

/**
* Quote a string so it is safe to interpolate into a POSIX shell (bash/zsh/sh).
* Wraps the value in single quotes and encodes any embedded single quote as
* `'\''`, leaving all other characters (including `"`, `$`, `` ` ``, `\`)
* literal. Used when generating env.sh, which every team member sources.
*/
function shellQuoteValue(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}

// ─── Handler ─────────────────────────────────────────────

export class EnvHandler extends ResourceHandler {
Expand Down Expand Up @@ -173,9 +183,15 @@ export class EnvHandler extends ResourceHandler {

/**
* Generate the content of ~/.teamai/env.sh with export statements.
*
* Values are single-quoted so shell metacharacters in an env value (quotes,
* `$`, backticks, `\`, …) are taken literally and cannot break or inject into
* the sourced script. An embedded single quote is encoded with the standard
* `'\''` sequence. env.sh is sourced from every team member's shell profile,
* so values (which originate from the team repo's env/env.yaml) must be safe.
*/
generateEnvFile(variables: EnvVariable[]): string {
const lines = variables.map(v => `export ${v.key}="${v.value}"`);
const lines = variables.map(v => `export ${v.key}=${shellQuoteValue(v.value)}`);
return lines.join('\n') + '\n';
}

Expand Down
Loading