Skip to content

Commit a8e691f

Browse files
koki-developclaude
andcommitted
feat: Add config file support for API key persistence
Allow storing the API key in ~/.config/codize/config.json so users don't need to pass --api-key or set CODIZE_API_KEY on every invocation. Adds `codize config` command with set/get/list/path subcommands. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5af87b1 commit a8e691f

File tree

6 files changed

+179
-10
lines changed

6 files changed

+179
-10
lines changed

CLAUDE.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ npx prettier --check .
3535
## Architecture
3636

3737
```
38-
src/index.ts — Entry point: sets up Commander program, registers commands, handles top-level errors
39-
src/error.ts — CliError class (extends Error with exitCode)
40-
src/commands/run.ts — `codize run` command: reads files, calls CodizeClient.sandbox.execute(), outputs results
41-
scripts/build.ts — Build script using Bun.build API
38+
src/index.ts — Entry point: sets up Commander program, registers commands, handles top-level errors
39+
src/error.ts — CliError class (extends Error with exitCode)
40+
src/config.ts — Config file read/write (path resolution, JSON parsing, validation)
41+
src/commands/run.ts — `codize run` command: reads files, calls CodizeClient.sandbox.execute(), outputs results
42+
src/commands/config.ts — `codize config` command: set/get/list/path subcommands for CLI configuration
43+
scripts/build.ts — Build script using Bun.build API
4244
```
4345

4446
### Command Registration Pattern
4547

46-
Each command is defined in `src/commands/` and exports a `register*Command(program)` function that is called from `src/index.ts`. The `run` command is currently the only command.
48+
Each command is defined in `src/commands/` and exports a `register*Command(program)` function that is called from `src/index.ts`.
4749

4850
### Error Handling
4951

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ $ npx @codize/cli --help
1717

1818
## Usage
1919

20-
### Authentication
20+
### Configuration
2121

22-
Generate your API key at [codize.dev/settings/api-keys](https://codize.dev/settings/api-keys), then set the `CODIZE_API_KEY` environment variable or pass the `--api-key` flag:
22+
Generate your API key at [codize.dev/settings/api-keys](https://codize.dev/settings/api-keys), then configure it using one of the following methods (listed in priority order):
23+
24+
1. **CLI flag:** `--api-key`
25+
2. **Environment variable:** `CODIZE_API_KEY`
26+
3. **Config file:** `codize config set api-key <key>`
2327

2428
```bash
29+
# Save API key to config file (recommended)
30+
$ codize config set api-key cdz_YourApiKeyHere
31+
32+
# Or use an environment variable
2533
$ export CODIZE_API_KEY="cdz_YourApiKeyHere"
2634
```
2735

36+
The config file is stored at `$XDG_CONFIG_HOME/codize/config.json` (defaults to `~/.config/codize/config.json`).
37+
2838
### Running a File
2939

3040
```bash

src/commands/config.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Command } from "commander";
2+
import type { Config } from "../config.ts";
3+
import {
4+
CONFIG_KEY_MAP,
5+
getConfigFilePath,
6+
readConfig,
7+
writeConfig,
8+
} from "../config.ts";
9+
import { CliError } from "../error.ts";
10+
11+
function resolveConfigKey(key: string): keyof Config {
12+
const prop = CONFIG_KEY_MAP[key];
13+
if (prop == null) {
14+
throw new CliError(
15+
`Unknown config key '${key}'. Valid keys: ${Object.keys(CONFIG_KEY_MAP).join(", ")}.`,
16+
);
17+
}
18+
return prop;
19+
}
20+
21+
export function registerConfigCommand(program: Command): void {
22+
const configCmd = program
23+
.command("config")
24+
.description("Manage CLI configuration");
25+
26+
configCmd
27+
.command("set <key> <value>")
28+
.description("Set a config value")
29+
.action((key: string, value: string) => {
30+
const prop = resolveConfigKey(key);
31+
const config = readConfig();
32+
config[prop] = value;
33+
writeConfig(config);
34+
process.stdout.write(`Set ${key}.\n`);
35+
});
36+
37+
configCmd
38+
.command("get <key>")
39+
.description("Get a config value")
40+
.action((key: string) => {
41+
const prop = resolveConfigKey(key);
42+
const config = readConfig();
43+
const val = config[prop];
44+
if (val == null) {
45+
throw new CliError(`Config key '${key}' is not set.`);
46+
}
47+
process.stdout.write(`${val}\n`);
48+
});
49+
50+
configCmd
51+
.command("list")
52+
.description("List all config values")
53+
.action(() => {
54+
const config = readConfig();
55+
for (const [cliKey, prop] of Object.entries(CONFIG_KEY_MAP)) {
56+
const val = config[prop];
57+
process.stdout.write(`${cliKey}=${val ?? "(not set)"}\n`);
58+
}
59+
});
60+
61+
configCmd
62+
.command("path")
63+
.description("Show the config file path")
64+
.action(() => {
65+
process.stdout.write(`${getConfigFilePath()}\n`);
66+
});
67+
}

src/commands/run.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
22
import { basename, extname } from "node:path";
33
import { CodizeApiError, CodizeClient } from "@codize/sdk";
44
import type { Command } from "commander";
5+
import { readConfig } from "../config.ts";
56
import { CliError } from "../error.ts";
67

78
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
@@ -94,10 +95,12 @@ export function registerRunCommand(program: Command): void {
9495
);
9596
}
9697

97-
const apiKey = options.apiKey ?? process.env["CODIZE_API_KEY"];
98+
const config = readConfig();
99+
const apiKey =
100+
options.apiKey ?? process.env["CODIZE_API_KEY"] ?? config.apiKey;
98101
if (!apiKey) {
99102
throw new CliError(
100-
"API key is required. Set CODIZE_API_KEY or use --api-key.",
103+
"API key is required. Use --api-key, set CODIZE_API_KEY, or run `codize config set api-key <key>`.",
101104
);
102105
}
103106

src/config.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2+
import { homedir } from "node:os";
3+
import { dirname, join } from "node:path";
4+
import { CliError } from "./error.ts";
5+
6+
export interface Config {
7+
apiKey?: string;
8+
}
9+
10+
/** Maps CLI key names (e.g. "api-key") to Config property names (e.g. "apiKey"). */
11+
export const CONFIG_KEY_MAP: Record<string, keyof Config> = {
12+
"api-key": "apiKey",
13+
};
14+
15+
function errorMessage(err: unknown): string {
16+
return err instanceof Error ? err.message : String(err);
17+
}
18+
19+
export function getConfigFilePath(): string {
20+
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
21+
const base =
22+
xdgConfigHome != null && xdgConfigHome !== ""
23+
? xdgConfigHome
24+
: join(homedir(), ".config");
25+
return join(base, "codize", "config.json");
26+
}
27+
28+
export function readConfig(): Config {
29+
const filePath = getConfigFilePath();
30+
if (!existsSync(filePath)) {
31+
return {};
32+
}
33+
let raw: string;
34+
try {
35+
raw = readFileSync(filePath, "utf-8");
36+
} catch (err) {
37+
throw new CliError(
38+
`Cannot read config file '${filePath}': ${errorMessage(err)}`,
39+
);
40+
}
41+
let parsed: unknown;
42+
try {
43+
parsed = JSON.parse(raw);
44+
} catch {
45+
throw new CliError(
46+
`Config file '${filePath}' contains invalid JSON. Fix or delete it to continue.`,
47+
);
48+
}
49+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
50+
throw new CliError(
51+
`Config file '${filePath}' must be a JSON object. Fix or delete it to continue.`,
52+
);
53+
}
54+
const obj = parsed as Record<string, unknown>;
55+
const config: Config = {};
56+
if ("apiKey" in obj && typeof obj["apiKey"] !== "string") {
57+
throw new CliError(
58+
`Config file '${filePath}': 'apiKey' must be a string, got ${typeof obj["apiKey"]}. Fix or delete it to continue.`,
59+
);
60+
}
61+
if (typeof obj["apiKey"] === "string") {
62+
config.apiKey = obj["apiKey"];
63+
}
64+
return config;
65+
}
66+
67+
export function writeConfig(config: Config): void {
68+
const filePath = getConfigFilePath();
69+
const dir = dirname(filePath);
70+
try {
71+
mkdirSync(dir, { recursive: true });
72+
} catch (err) {
73+
throw new CliError(
74+
`Cannot create config directory '${dir}': ${errorMessage(err)}`,
75+
);
76+
}
77+
const json = JSON.stringify(config, null, 2) + "\n";
78+
try {
79+
writeFileSync(filePath, json, "utf-8");
80+
} catch (err) {
81+
throw new CliError(
82+
`Cannot write config file '${filePath}': ${errorMessage(err)}`,
83+
);
84+
}
85+
}

src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Command } from "commander";
22
import packageJson from "../package.json" with { type: "json" };
3-
import { CliError } from "./error.ts";
3+
import { registerConfigCommand } from "./commands/config.ts";
44
import { registerRunCommand } from "./commands/run.ts";
5+
import { CliError } from "./error.ts";
56

67
const program = new Command();
78

@@ -11,6 +12,7 @@ program
1112
.version(packageJson.version);
1213

1314
registerRunCommand(program);
15+
registerConfigCommand(program);
1416

1517
program.parseAsync(process.argv).catch((err: unknown) => {
1618
const message =

0 commit comments

Comments
 (0)