feat: Add cloud info command (spawn <cloud>) for cloud-first discovery (#192)

Users who know their preferred cloud but not which agents are available
had no way to find out. Now `spawn hetzner` shows all agents available
on Hetzner, mirroring how `spawn claude` shows all clouds for Claude.

- Add cmdCloudInfo() showing cloud details + available agents
- handleDefaultCommand detects cloud names and routes to cloud info
- Update help text and clouds list footer to document the new command
- Bump CLI version to 0.2.15

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-10 08:54:24 -08:00 committed by GitHub
parent 17e78b9f04
commit d1ecbaf88c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 67 additions and 4 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.2.14",
"version": "0.2.15",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -459,7 +459,7 @@ export async function cmdClouds(): Promise<void> {
console.log(` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${c.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${implCount} agent${implCount !== 1 ? "s" : ""} ${c.description}`)}`);
}
console.log();
console.log(pc.dim(` Run ${pc.cyan("spawn <agent> <cloud>")} to launch.`));
console.log(pc.dim(` Run ${pc.cyan("spawn <cloud>")} for details, or ${pc.cyan("spawn <agent> <cloud>")} to launch.`));
console.log();
}
@ -491,6 +491,53 @@ export async function cmdAgentInfo(agent: string): Promise<void> {
console.log();
}
// ── Cloud Info ─────────────────────────────────────────────────────────────────
// Validate and load cloud - consolidates the pattern used by cmdCloudInfo
async function validateAndGetCloud(cloud: string): Promise<[manifest: Manifest, cloudKey: string]> {
try {
validateIdentifier(cloud, "Cloud name");
} catch (err) {
p.log.error(getErrorMessage(err));
process.exit(1);
}
validateNonEmptyString(cloud, "Cloud name", "spawn clouds");
const manifest = await loadManifestWithSpinner();
validateCloud(manifest, cloud);
return [manifest, cloud];
}
export async function cmdCloudInfo(cloud: string): Promise<void> {
const [manifest, cloudKey] = await validateAndGetCloud(cloud);
const c = manifest.clouds[cloudKey];
console.log();
console.log(`${pc.bold(c.name)} ${pc.dim("--")} ${c.description}`);
if (c.notes) {
console.log(pc.dim(` ${c.notes}`));
}
console.log();
console.log(pc.bold("Available agents:"));
console.log();
let found = false;
for (const agent of agentKeys(manifest)) {
const status = matrixStatus(manifest, cloudKey, agent);
if (status === "implemented") {
const a = manifest.agents[agent];
console.log(` ${pc.green(agent.padEnd(NAME_COLUMN_WIDTH))} ${a.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim("spawn " + agent + " " + cloudKey)}`);
found = true;
}
}
if (!found) {
console.log(pc.dim(" No implemented agents yet."));
}
console.log();
}
// ── Update ─────────────────────────────────────────────────────────────────────
export async function cmdUpdate(): Promise<void> {
@ -549,6 +596,7 @@ ${pc.bold("USAGE")}
spawn <agent> <cloud> --prompt-file <file>
Execute agent with prompt from file
spawn <agent> Show available clouds for agent
spawn <cloud> Show available agents for cloud
spawn list Full matrix table
spawn agents List all agents with descriptions
spawn clouds List all cloud providers
@ -566,6 +614,7 @@ ${pc.bold("EXAMPLES")}
spawn claude sprite --prompt-file instructions.txt
${pc.dim("# Read prompt from file")}
spawn claude ${pc.dim("# Show which clouds support Claude")}
spawn hetzner ${pc.dim("# Show which agents run on Hetzner")}
spawn list ${pc.dim("# See the full agent x cloud matrix")}
${pc.bold("AUTHENTICATION")}

View file

@ -6,11 +6,13 @@ import {
cmdAgents,
cmdClouds,
cmdAgentInfo,
cmdCloudInfo,
cmdUpdate,
cmdHelp,
} from "./commands.js";
import pkg from "../package.json" with { type: "json" };
import { checkForUpdates } from "./update-check.js";
import { loadManifest } from "./manifest.js";
const VERSION = pkg.version;
@ -56,7 +58,13 @@ const HELP_FLAGS = ["--help", "-h", "help"];
async function handleDefaultCommand(agent: string, cloud: string | undefined, prompt?: string): Promise<void> {
// Handle "spawn <agent> --help" / "spawn <agent> -h" / "spawn <agent> help"
if (cloud && HELP_FLAGS.includes(cloud)) {
await cmdAgentInfo(agent);
// Could be "spawn <agent> --help" or "spawn <cloud> --help"
const manifest = await loadManifest();
if (!manifest.agents[agent] && manifest.clouds[agent]) {
await cmdCloudInfo(agent);
} else {
await cmdAgentInfo(agent);
}
return;
}
if (cloud) {
@ -67,7 +75,13 @@ async function handleDefaultCommand(agent: string, cloud: string | undefined, pr
console.error(`\nUsage: spawn ${agent} <cloud> --prompt "your prompt here"`);
process.exit(1);
}
await cmdAgentInfo(agent);
// "spawn <name>" with no second arg: show agent info, or cloud info if it's a cloud name
const manifest = await loadManifest();
if (!manifest.agents[agent] && manifest.clouds[agent]) {
await cmdCloudInfo(agent);
} else {
await cmdAgentInfo(agent);
}
}
}