@agent-ix/ix-cli-core
v0.10.2
Published
Generic framework foundation for building Agent IX CLIs — config service, secrets service, plugin contract, runtime.
Readme
@agent-ix/ix-cli-core
Generic framework foundation for building Agent IX CLIs — config service, secrets service, plugin contract, runtime.
ix-cli-core is the shared substrate every Agent IX command-line tool is built on. It takes the parts of a CLI that are tedious and easy to get wrong — typed configuration, secret storage, login, plugin loading, command wiring — and provides them as a single, batteries-included library on top of oclif. Your CLI declares what it needs; the framework handles how it is stored, validated, resolved, and secured.
pnpm add @agent-ix/ix-cli-coreRequires
@oclif/core(peer dependency)>= 4.11.4.
Features
🗂 Typed configuration
Plugin-scoped configuration files backed by Zod schemas. Each plugin owns its own YAML file under the user's config directory, and every read is validated and type-safe.
- Schema-validated — values are parsed against a strict Zod schema; unknown keys are rejected.
- Layered resolution — environment variables override file values, which override schema defaults.
- Project-local overrides — an in-repo
.ix/directory can layer per-project settings over the user config. - Crash-proof reads — a corrupt or invalid file never throws into your command; defaults are returned and the problem is recorded for diagnostics.
- Safe writes — every write is atomic, permission-locked (
0o600), and serialized against concurrent writers. - Built-in diagnostics — a
doctorreport surfaces parse, schema, and I/O incidents across all plugins.
🔐 Multi-backend secrets
Secret storage that does the right thing on every platform without the caller caring where bytes actually live.
- OS-native keyring first — uses the macOS Keychain, Windows Credential Manager, or Linux Secret Service when available.
- Encrypted file fallback — transparently falls back to an age-encrypted on-disk store when no keyring is present.
- Environment overrides — a declared env var always wins, so CI and containers can inject secrets without touching disk.
- Leak-resistant — secret values are kept out of logs and error messages; only ids and descriptions are ever rendered.
🔑 Auth engine (device flow)
A drop-in OAuth 2.0 Device Authorization Grant (RFC 8628) login, fully service-agnostic.
- Service discovery — reads a
/.well-known/agentix-service.jsondocument so endpoints, audience, and scopes are never hard-coded. - Browser-based approval — walks the user through verification with a best-effort, non-fatal browser open.
- Host-isolated tokens — tokens are keyed per host and stored through the secrets backend; logging into one service never disturbs another.
- Automatic refresh — access tokens are refreshed before expiry and rotated transparently.
🧩 Plugin contract
A lightweight convention that lets a plugin declare everything the framework needs in one object.
- A plugin exports an
ixSchemadescribing its config schema, secrets, and env bindings. - The host registers it once at startup, and the plugin's config and secrets immediately become visible to
ix configandix secrets. - Plugin namespaces are derived safely from the npm package name when not given explicitly.
⚙️ Base command + capabilities
An oclif base command that every command in your CLI extends to inherit framework wiring for free.
- Standard flags —
--config-rootand--no-project-configare parsed natively, no argv hacks. - Ready-to-use services — config and secrets are available without per-command boilerplate.
- Declarative capability checks — a command states the capabilities it requires (e.g. "needs auth"), and the framework short-circuits with a structured, user-friendly error when they are unavailable.
📦 Plugin marketplace
A thin adapter over @agent-ix/ts-plugin-kit for installing and reconciling plugins from typed git sources, with a cache layout under the standard cache directory.
🛡 Safe filesystem & XDG paths
The primitives the rest of the framework is built on: XDG-compliant config and cache locations, and atomic writes that enforce 0o600 permissions and refuse to follow symlinks.
Usage Guide
This walkthrough builds up a CLI on the framework, one subsystem at a time. Everything is imported from the package root:
import {
ConfigService,
registerPluginSchema,
registerSecret,
SecretsService,
defaultSecretsService,
fetchServiceDiscovery,
runDeviceFlow,
TokenStore,
BaseCommand,
} from "@agent-ix/ix-cli-core";
import { z } from "zod";1. Declare your plugin schema
A plugin describes its config, secrets, and env bindings in a single ixSchema object and registers it at startup. Config schemas must be strict Zod objects.
const ixSchema = {
id: "deploy", // config/secrets namespace; derived from the package name if omitted
config: z
.object({
region: z.string().default("us-east-1"),
replicas: z.coerce.number().default(1),
})
.strict(),
secrets: [
{
name: "api-token",
description: "Deploy API token",
envVar: "DEPLOY_API_TOKEN",
},
],
env: { region: "DEPLOY_REGION" }, // config key → env var
};
const result = registerPluginSchema("@acme/ix-cli-deploy", ixSchema);
if (!result.ok) {
// first-wins, non-throwing: inspect result.kind / result.detail
console.warn(`plugin schema not registered: ${result.detail}`);
}Registering the schema also wires its config and secrets into the global registries, so ix config and ix secrets see the plugin immediately.
2. Read and write typed config
Get a typed accessor scoped to one plugin id. get() resolves env → file → defaults; set() deep-merges and atomically rewrites.
const config = ConfigService.forPlugin("deploy", ixSchema.config, {
envBindings: { region: "DEPLOY_REGION" },
});
const current = config.get(); // { region: string; replicas: number } — fully typed
config.set({ replicas: 3 }); // validated, atomic, lock-serialized write
config.replace({ region: "eu-west-1", replicas: 2 }); // overwrite (can remove keys)
config.reset(); // delete the file; get() then returns defaultsDiagnose problems across all registered plugins:
import { doctor } from "@agent-ix/ix-cli-core";
const report = doctor(); // parse / schema / io incidents, per plugin3. Store and read secrets
Secrets are declared (so they appear in ix secrets list) and then read or written through a SecretsService. The framework picks the backend; in most commands you use the process-global default.
registerSecret("deploy", {
name: "api-token",
description: "Deploy API token",
envVar: "DEPLOY_API_TOKEN",
});
const secrets = defaultSecretsService();
await secrets.set("deploy.api-token", "s3cr3t"); // refuses if env var is shadowing
const token = await secrets.get("deploy.api-token"); // env var > backend > null
const source = await secrets.which("deploy.api-token"); // "env" | "keyring" | "age-file" | "unset"get() returns the env-var value first when the secret's envVar is set, then falls back to the active backend, then null.
4. Wire up login
Discover the service, run the device flow, and persist the resulting tokens in a host-keyed TokenStore (backed by the secrets service).
const discovery = await fetchServiceDiscovery("https://api.example.com");
const bundle = await runDeviceFlow(discovery, {
clientId: "my-cli",
prompter: {
showVerification({ approvalUri, userCode }) {
console.log(`Visit ${approvalUri} and enter code ${userCode}`);
},
},
});
const tokens = new TokenStore({ secrets: defaultSecretsService() });
await tokens.save("https://api.example.com", bundle);
// Later, in any command — refreshed automatically when near expiry:
const accessToken = await tokens.getAccessToken("https://api.example.com");By default runDeviceFlow opens the verification URI in a browser (non-fatal) and presents the prompt through the prompter you supply, so your CLI owns all rendering.
5. Author a command
Extend BaseCommand to inherit the global flags and runtime context, and declare any capabilities the command requires. A missing required capability short-circuits the command with a structured error before run() executes.
export default class Deploy extends BaseCommand {
static override description = "Deploy the current project";
static override capabilities = {
required: ["ix-api"], // resolved in prerun(); fails fast if unavailable
optional: ["github"],
} as const;
async run(): Promise<void> {
const config = ConfigService.forPlugin("deploy", ixSchema.config);
const { region, replicas } = config.get();
this.log(`Deploying ${replicas} replica(s) to ${region}…`);
if (this.hasCapability("github")) {
// optional capability is available — do the extra thing
}
}
}6. Install marketplace plugins (optional)
For CLIs that load plugins from typed git sources, the marketplace adapter provides install options and a reconcile pass over a default set:
import {
marketplaceInstallOptions,
reconcileDefaultSet,
} from "@agent-ix/ix-cli-core";
const opts = marketplaceInstallOptions();
await reconcileDefaultSet(/* targets */);Related projects
- ix-cli — the canonical Agent IX CLI (
ix), built on this framework. (Currently private; will be made public soon.) - quoin — the spec authoring and validation toolchain for the Agent IX ecosystem.
- ix-flow — the agent-driven workflow lifecycle runner.
Development
This project uses pnpm with Corepack.
Prerequisites
- Node.js 20+
- Corepack enabled (
corepack enable)
Setup
pnpm install # install dependencies
pnpm run build # build the library (vite)
pnpm run test # run the test suite (vitest)Scripts
| Command | Description |
| ----------------- | --------------------------------------- |
| pnpm run build | Compile the library |
| pnpm test | Run tests |
| pnpm run lint | Run ESLint + Prettier check |
| pnpm run format | Run Prettier |
| pnpm run clean | Remove build artifacts and node_modules |
A Makefile is provided for convenience and delegates to the equivalent pnpm run scripts (make build, make test, make lint, …).
License
MIT © Agent IX
