Skip to content
Open
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
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"fedi",
"fedibird",
"fedify",
"fedifyrc",
"fediverse",
"followable",
"Guppe",
Expand Down
95 changes: 95 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,101 @@ the appropriate executable for your platform and put it in your `PATH`.
[releases]: https://github.com/fedify-dev/fedify/releases


Configuration
-------------

The `fedify` command can be configured via configuration files. This allows
you to set default options for various commands without having to specify them
on the command line every time.

To disable loading configuration files, you can use the `--no-config` global
option.

### Configuration Files

`fedify` looks for configuration files with the following names:

- `.fedifyrc`
- `fedify.config.json`

The files are expected to be in JSON format.

### Configuration File Locations

The `fedify` command searches for configuration files in the following
locations, in order:

1. **Current Directory**: The directory from which you run the `fedify`
command.
- `./.fedifyrc`
- `./fedify.config.json`

2. **System-Specific Configuration Directory**:
- **Linux and macOS**: Based on the [XDG Base Directory Specification],
`fedify` looks in `$XDG_CONFIG_HOME/fedify/`. If `$XDG_CONFIG_HOME`
is not set, it defaults to `~/.config/fedify/`.
- `$XDG_CONFIG_HOME/fedify/.fedifyrc`
- `$XDG_CONFIG_HOME/fedify/fedify.config.json`
- **Windows**: `fedify` looks in the directory obtained from the `APPDATA`
environment variable. If `APPDATA` is not set, it falls back to
`%USERPROFILE%\AppData\Roaming`. The configuration directory will be
`APPDATA\fedify\`.
- `%APPDATA%\fedify\.fedifyrc`
- `%APPDATA%\fedify\fedify.config.json`

`fedify` will use the first configuration file it finds and stop searching.

[XDG Base Directory Specification]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

### Precedence

The settings are applied with the following precedence, from highest to lowest:

1. Command-line options (e.g., `--cache-dir`, `--verbose`).
2. Configuration file in the current working directory (`.fedifyrc` takes
precedence over `fedify.config.json`).
3. Configuration file in the system-specific configuration directory
(`.fedifyrc` takes precedence over `fedify.config.json`).

For example, if `cacheDir` is specified in a configuration file, but you also
provide the `--cache-dir` option on the command line, the value from the
command line option will be used.

### Available Fields

The following fields are available in the configuration file:

~~~~ json
{
"cacheDir": "/path/to/cache",
"verbose": true,
"http": {
"timeout": 30000,
"userAgent": "MyFedifyClient/1.0",
"followRedirects": true
},
"format": {
"default": "json"
}
}
~~~~

- `cacheDir` (string): Path to the cache directory. Corresponds to the global
`--cache-dir` option.
- `verbose` (boolean): Enable verbose output. Corresponds to the global
`-v`/`--verbose` option.
- `http` (object): HTTP-related settings.
- `timeout` (number): Timeout for HTTP requests in milliseconds.
- `userAgent` (string): The `User-Agent` header for HTTP requests.
Corresponds to the `--user-agent` option in commands like `lookup` and
`nodeinfo`.
- `followRedirects` (boolean): Whether to automatically follow HTTP
redirects (e.g., 301, 302, 307, 308 status codes) when making requests.
Defaults to `true`.
- `format` (object): Output formatting options for commands like `lookup`.
- `default` (string): Default output format. Can be `json`, `json-ld`, or
`yaml`.

`fedify init`: Initializing a Fedify project
--------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@std/assert": "jsr:@std/assert@^1.0.13",
"@std/fmt/colors": "jsr:@std/fmt@^0.224.0/colors",
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.6",
"@std/semver": "jsr:@std/semver@^1.0.5",
"cli-highlight": "npm:cli-highlight@^2.1.11",
"fetch-mock": "npm:fetch-mock@^12.5.2",
Expand Down
211 changes: 211 additions & 0 deletions packages/cli/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { assertEquals } from "@std/assert";
import { join } from "@std/path";
import type { Config } from "./config.ts";
import { loadConfig } from "./config.ts";

async function withTempEnv(
testFn: (
tempDir: string,
homeDir: string,
) => Promise<void> | void,
) {
const tempDir = await Deno.makeTempDir();
const originalCwd = Deno.cwd();
const originalHome = Deno.env.get("HOME");
const originalXdgConfigHome = Deno.env.get("XDG_CONFIG_HOME");
const originalAppData = Deno.env.get("APPDATA");
const originalUserProfile = Deno.env.get("USERPROFILE");

const homeDir = join(tempDir, "home");
await Deno.mkdir(homeDir, { recursive: true });
Deno.env.set("HOME", homeDir);
Deno.env.set("USERPROFILE", homeDir);

const xdgConfigDir = join(homeDir, ".config");
await Deno.mkdir(xdgConfigDir, { recursive: true });
Deno.env.set("XDG_CONFIG_HOME", xdgConfigDir);

const appDataDir = join(homeDir, "AppData", "Roaming");
await Deno.mkdir(appDataDir, { recursive: true });
Deno.env.set("APPDATA", appDataDir);

try {
Deno.chdir(tempDir);
await testFn(tempDir, homeDir);
} finally {
Deno.chdir(originalCwd);
if (originalHome) {
Deno.env.set("HOME", originalHome);
} else {
Deno.env.delete("HOME");
}
if (originalXdgConfigHome) {
Deno.env.set("XDG_CONFIG_HOME", originalXdgConfigHome);
} else {
Deno.env.delete("XDG_CONFIG_HOME");
}
if (originalAppData) {
Deno.env.set("APPDATA", originalAppData);
} else {
Deno.env.delete("APPDATA");
}
if (originalUserProfile) {
Deno.env.set("USERPROFILE", originalUserProfile);
} else {
Deno.env.delete("USERPROFILE");
}
await Deno.remove(tempDir, { recursive: true });
}
}

async function createConfigFile(
dir: string,
filename: string,
config: Config | string,
) {
const content = typeof config === "string" ? config : JSON.stringify(config);
await Deno.writeTextFile(join(dir, filename), content);
}

Deno.test("loadConfig()", async (t) => {
await t.step(
"should return an empty object if no config file is found",
async () => {
await withTempEnv(async () => {
const config = await loadConfig();
assertEquals(config, {});
});
},
);

await t.step(
"should load config from .fedifyrc in current directory",
async () => {
await withTempEnv(async (tempDir) => {
const testConfig: Config = { http: { timeout: 5000 } };
await createConfigFile(tempDir, ".fedifyrc", testConfig);
const config = await loadConfig();
assertEquals(config, testConfig);
});
},
);

await t.step(
"should load config from fedify.config.json in current directory",
async () => {
await withTempEnv(async (tempDir) => {
const testConfig: Config = { verbose: true };
await createConfigFile(
tempDir,
"fedify.config.json",
testConfig,
);
const config = await loadConfig();
assertEquals(config, testConfig);
});
},
);

await t.step(
"should prioritize .fedifyrc over fedify.config.json in current directory",
async () => {
await withTempEnv(async (tempDir) => {
const rcConfig: Config = { http: { userAgent: "test-rc" } };
const jsonConfig: Config = { http: { userAgent: "test-json" } };
await createConfigFile(tempDir, ".fedifyrc", rcConfig);
await createConfigFile(
tempDir,
"fedify.config.json",
jsonConfig,
);
const config = await loadConfig();
assertEquals(config, rcConfig);
});
},
);

await t.step(
"should load config from .fedifyrc in home directory (XDG)",
async () => {
await withTempEnv(async () => {
const testConfig: Config = { format: { default: "yaml" } };
const configPath = join(
Deno.env.get("XDG_CONFIG_HOME")!,
"fedify",
);
await Deno.mkdir(configPath, { recursive: true });
await createConfigFile(configPath, ".fedifyrc", testConfig);
const config = await loadConfig("linux");
assertEquals(config, testConfig);
});
},
);

await t.step(
"should load config from .fedifyrc in home directory (Windows)",
async () => {
await withTempEnv(async () => {
const testConfig: Config = { format: { default: "yaml" } };
const configPath = join(Deno.env.get("APPDATA")!, "fedify");
await Deno.mkdir(configPath, { recursive: true });
await createConfigFile(configPath, ".fedifyrc", testConfig);
const config = await loadConfig("windows");
assertEquals(config, testConfig);
});
},
);

await t.step(
"should prioritize current directory over home directory",
async () => {
await withTempEnv(async (tempDir) => {
const currentDirConfig: Config = { cacheDir: "./current" };
const homeDirConfig: Config = { cacheDir: "./home" };
const homeConfigPath = join(
Deno.env.get("XDG_CONFIG_HOME")!,
"fedify",
);
await Deno.mkdir(homeConfigPath, { recursive: true });
await createConfigFile(tempDir, ".fedifyrc", currentDirConfig);
await createConfigFile(homeConfigPath, ".fedifyrc", homeDirConfig);
const config = await loadConfig("linux");
assertEquals(config, currentDirConfig);
});
},
);

await t.step(
"should ignore malformed config and continue searching",
async () => {
await withTempEnv(async (tempDir) => {
const jsonConfig: Config = { verbose: false };
await createConfigFile(tempDir, ".fedifyrc", "not json");
await createConfigFile(
tempDir,
"fedify.config.json",
jsonConfig,
);
const config = await loadConfig();
assertEquals(config, jsonConfig);
});
},
);
});

Deno.test("loadConfig() applies cacheDir correctly", async () => {
await withTempEnv(async (tempDir) => {
const testConfig = { cacheDir: "./test-cache" };
await createConfigFile(tempDir, "fedify.config.json", testConfig);
const config = await loadConfig();
assertEquals(config.cacheDir, "./test-cache");
});
});

Deno.test("loadConfig() applies verbose correctly", async () => {
await withTempEnv(async (tempDir) => {
const testConfig = { verbose: true };
await createConfigFile(tempDir, "fedify.config.json", testConfig);
const config = await loadConfig();
assertEquals(config.verbose, true);
});
});
Loading
Loading