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
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://img.shields.io/badge/jato-v0.1.0-e0944a?style=flat-square&labelColor=1a1a1a" alt="version" />
<img src="https://img.shields.io/npm/v/@malopezr7/jato?style=flat-square&labelColor=1a1a1a&color=e0944a" alt="version" />
<img src="https://img.shields.io/github/license/malopezr7/jato?style=flat-square&labelColor=1a1a1a&color=918e88" alt="license" />
<img src="https://img.shields.io/github/stars/malopezr7/jato?style=flat-square&labelColor=1a1a1a&color=918e88" alt="stars" />
<img src="https://img.shields.io/badge/node-%3E%3D22-918e88?style=flat-square&labelColor=1a1a1a" alt="node" />
Expand All @@ -17,7 +17,7 @@
<br />

```bash
npx jato init
npx @malopezr7/jato init
```

---
Expand All @@ -34,7 +34,7 @@ The real trick: jato installs a **skill that teaches your LLM to be your config

```bash
# Install and create your first jato
npx jato init
npx @malopezr7/jato init

# Activate it
jato use my-jato
Expand All @@ -48,6 +48,23 @@ Three init paths:
- **Template** — starts from a built-in template (`starter`, `mobile`, `backend`, `fullstack`)
- **Wizard** — just ask your AI to build one (the jato-manager skill handles it)

## Quick Skill Setup

Want the jato-manager skill in your AI tools right now, without setting up a full jato?

```bash
# Install the skill to all detected providers
npx @malopezr7/jato skill install

# Install to a specific provider only
npx @malopezr7/jato skill install --provider claude

# Install to multiple providers
npx @malopezr7/jato skill install --provider claude --provider gemini
```

This copies the **jato-manager** skill directly into each provider's skills directory (`~/.claude/skills/`, `~/.gemini/skills/`, etc.), so your LLM can immediately help you manage jato configurations.

## How It Works

A jato is a folder. Everything inside is your AI setup:
Expand Down Expand Up @@ -96,6 +113,7 @@ jato list Show available jatos
jato off Deactivate the current jato
jato doctor Health check — schema, env vars, providers, skills
jato install <repo> Install jatos from a git repository
jato skill install Install the jato-manager skill to your AI providers
```

That's the whole surface.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "jato",
"name": "@malopezr7/jato",
"version": "0.1.0",
"description": "One directory. Every AI tool. Centralized configuration for Claude Code, Codex CLI, Gemini CLI, and OpenCode.",
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion site/src/components/CLI.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const commands = [
{ c: "jato off", d: "Deactivate the current jato" },
{ c: "jato doctor", d: "Health check — schema, env vars, providers, skills" },
{ c: "jato install <repo>", d: "Install jatos from a git repository" },
{ c: "jato skill install", d: "Quick-install the jato-manager skill into your AI providers" },
];
---

Expand All @@ -15,7 +16,7 @@ const commands = [
</div>
<div class="fade-in" style="transition-delay: 0.05s">
<h2 class="section-title">
Six commands.<br /><span class="section-muted">That's the whole surface.</span>
Seven commands.<br /><span class="section-muted">That's the whole surface.</span>
</h2>
</div>
<div class="font-mono text-sm flex flex-col">
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/CTA.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
<div
class="font-mono text-sm text-jato-btn-outline-text pl-7 pr-4 py-3 rounded-lg border border-jato-btn-outline-border transition-colors flex items-center gap-3"
>
<code class="select-all">npx jato init</code>
<code class="select-all">npx @malopezr7/jato init</code>
<button
onclick="navigator.clipboard.writeText('npx jato init')"
onclick="navigator.clipboard.writeText('npx @malopezr7/jato init')"
aria-label="Copy"
class="text-emerald-500 hover:text-emerald-400 transition-colors cursor-pointer bg-transparent border-none p-0"
>
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
>
<div class="flex items-center gap-2 pl-5 py-3">
<span class="text-jato-text-ghost select-none">$</span>
<code id="install-cmd" class="text-jato-text select-all">npx jato init</code>
<code id="install-cmd" class="text-jato-text select-all">npx @malopezr7/jato init</code>
</div>
<button
id="copy-btn"
Expand Down Expand Up @@ -80,7 +80,7 @@
if (!btn) return;

btn.addEventListener("click", function () {
navigator.clipboard.writeText("npx jato init").then(function () {
navigator.clipboard.writeText("npx @malopezr7/jato init").then(function () {
if (copyIcon) copyIcon.classList.add("hidden");
if (checkIcon) checkIcon.classList.remove("hidden");
setTimeout(function () {
Expand Down
97 changes: 97 additions & 0 deletions src/cli/commands/skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Command } from "commander";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import chalk from "chalk";
import { getSkillsDir } from "../../core/hub.js";
import { detectInstalledProviders } from "../../providers/detector.js";
import { getProvider } from "../../providers/registry.js";

async function readManagerSkill(home?: string): Promise<string> {
const hubPath = join(getSkillsDir(home), "jato-manager.md");
try {
return await readFile(hubPath, "utf8");
} catch {
// Fallback: read from dist/src
const __dirname = dirname(fileURLToPath(import.meta.url));
const distPath = join(__dirname, "..", "..", "skills", "jato-manager.md");
try {
return await readFile(distPath, "utf8");
} catch {
const srcPath = join(
__dirname,
"..",
"..",
"..",
"src",
"skills",
"jato-manager.md",
);
return await readFile(srcPath, "utf8");
}
}
}

export function registerSkillCommand(program: Command): void {
const skill = program
.command("skill")
.description("Manage jato skills");

skill
.command("install")
.description(
"Install the jato-manager skill directly into provider skills directories",
)
.option(
"--provider <names...>",
"Providers to install to (e.g. claude, codex, gemini). Defaults to all detected.",
)
.action(async (opts: { provider?: string[] }) => {
const content = await readManagerSkill();

let targetProviders: string[];

if (opts.provider) {
targetProviders = opts.provider;
} else {
const detected = await detectInstalledProviders();
targetProviders = detected
.filter((d) => d.found)
.map((d) => d.name);
}

if (targetProviders.length === 0) {
process.stderr.write(
` ${chalk.red("✗")} No AI providers detected. Use --provider to specify one.\n`,
);
process.exitCode = 1;
return;
}

let installed = 0;

for (const name of targetProviders) {
const provider = getProvider(name);
if (!provider) {
process.stderr.write(
` ${chalk.yellow("!")} Unknown provider: ${name}\n`,
);
continue;
}

const skillDir = join(provider.skillsDir(), "jato-manager");
await mkdir(skillDir, { recursive: true });
await writeFile(join(skillDir, "SKILL.md"), content, "utf8");
process.stdout.write(
` ${chalk.green("✓")} Installed jato-manager skill → ${name} (${skillDir})\n`,
);
installed++;
}

if (installed > 0) {
process.stdout.write(
`\n ${chalk.dim("Your AI tools can now help you manage jatos. Try asking:")} ${chalk.cyan('"Create a jato for my React project"')}\n`,
);
}
});
}
2 changes: 2 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { registerListCommand } from "./commands/list.js";
import { registerOffCommand } from "./commands/off.js";
import { registerDoctorCommand } from "./commands/doctor.js";
import { registerInstallCommand } from "./commands/install.js";
import { registerSkillCommand } from "./commands/skill.js";

export type CliResult = {
exitCode: number;
Expand All @@ -28,6 +29,7 @@ export function createProgram(): Command {
registerOffCommand(program);
registerDoctorCommand(program);
registerInstallCommand(program);
registerSkillCommand(program);

return program;
}
Expand Down
60 changes: 60 additions & 0 deletions tests/cli/skill.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";

let tmpHome: string;
const cliPath = join(process.cwd(), "dist", "cli", "index.js");

beforeEach(async () => {
tmpHome = await mkdtemp(join(tmpdir(), "rig-cli-skill-"));
});

afterEach(async () => {
await rm(tmpHome, { recursive: true, force: true });
});

function runCli(
args: string,
): { stdout: string; stderr: string; exitCode: number } {
const result = spawnSync("node", [cliPath, ...args.split(" ")], {
env: { ...process.env, HOME: tmpHome },
encoding: "utf8",
timeout: 10000,
});
return {
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
exitCode: result.status ?? 1,
};
}

describe("jato skill install", () => {
it("installs skill to a specific provider", async () => {
// Create the provider's skills dir so detection can work
await mkdir(join(tmpHome, ".claude"), { recursive: true });

const { stdout } = runCli("skill install --provider claude");
expect(stdout).toContain("Installed jato-manager skill");
expect(stdout).toContain("claude");

const skillContent = await readFile(
join(tmpHome, ".claude", "skills", "jato-manager", "SKILL.md"),
"utf8",
);
expect(skillContent).toContain("jato");
});

it("fails when no providers are detected and none specified", async () => {
const { stderr, exitCode } = runCli("skill install");
expect(exitCode).toBe(1);
expect(stderr).toContain("No AI providers detected");
});

it("warns on unknown provider", async () => {
const { stdout, stderr } = runCli("skill install --provider fakeprovider");
const output = stdout + stderr;
expect(output).toContain("Unknown provider");
});
});