원클릭으로
extension-installer
// Use when installing, creating, or setting up Freshell extensions — from GitHub repos, local directories, or from scratch as custom panes.
// Use when installing, creating, or setting up Freshell extensions — from GitHub repos, local directories, or from scratch as custom panes.
| name | extension-installer |
| description | Use when installing, creating, or setting up Freshell extensions — from GitHub repos, local directories, or from scratch as custom panes. |
Use this skill when a user wants to:
Do NOT use for modifying built-in pane types (terminal, browser, picker, etc.).
Read this box before doing anything. These are the non-obvious rules that cause silent failures.
Extensions must be pre-built. Freshell does NOT run npm install, npm run build, or any build step. The extension directory must contain ready-to-run artifacts before symlinking.
Scan only on startup. Extensions are discovered once when the server starts. After installing or changing an extension, Freshell must be restarted.
z.strictObject rejects unknown keys. The manifest schema uses strict validation. Any key not in the schema (typos, extra fields) causes the entire manifest to silently fail validation and the extension is skipped. Check server logs for warnings.
Exactly one category config block. The manifest must have exactly one of client, server, or cli — and it must match the category field. Having zero, two, or a mismatched block fails validation.
Symlinks are the recommended dev pattern. The scanner follows symlinks. Point ~/.freshell/extensions/<name> at your project directory for development.
Template interpolation in server.env. Values support {{port}} (allocated port) and {{varName}} (contentSchema field defaults). Unresolved templates are left as-is.
~/ expands to homedir. After template interpolation, env values starting with ~/ are expanded to the user's home directory.
Two scan directories. Freshell scans ~/.freshell/extensions/ (user-installed) and .freshell/extensions/ (local dev, relative to cwd). First match wins for duplicate names.
| Extension needs... | Category | Required config block |
|---|---|---|
| Its own HTTP server process (Express, Flask, etc.) | server | server: { command, ... } |
| Just static HTML/JS/CSS served by Freshell | client | client: { entry } |
| A TUI/CLI tool running in a terminal | cli | cli: { command, ... } |
All fields below are derived from the Zod schema in server/extension-manifest.ts. Use only these keys — any others cause silent rejection.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Unique identifier, min 1 char |
version | string | yes | Semver recommended, min 1 char |
label | string | yes | Human-readable display name |
description | string | yes | Short description for picker |
category | "client" | "server" | "cli" | yes | Must match the config block |
icon | string | no | Path to icon file (relative to extension dir) |
url | string | no | URL path template for iframe src (server and client extensions). Supports {{fieldName}} interpolation from contentSchema. Defaults to "/" |
contentSchema | object | no | Defines dynamic fields for pane props (see below) |
picker | object | no | Picker UI config (see below) |
client | object | conditional | Required when category: "client" |
server | object | conditional | Required when category: "server" |
cli | object | conditional | Required when category: "cli" |
contentSchema fieldsEach key in contentSchema maps to a field descriptor:
| Field | Type | Required | Notes |
|---|---|---|---|
type | "string" | "number" | "boolean" | yes | |
label | string | yes | Display label |
required | boolean | no | |
default | string | number | boolean | no | Must match the declared type (e.g., type: "string" requires a string default) |
picker fields| Field | Type | Required | Notes |
|---|---|---|---|
shortcut | string | no | Keyboard shortcut letter in picker |
group | string | no | Picker group name |
client config| Field | Type | Required | Notes |
|---|---|---|---|
entry | string | yes | Path to HTML file (relative to extension dir), min 1 char |
server config| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
command | string | yes | — | Executable to run (e.g., "node") |
args | string[] | no | [] | Arguments to command |
env | Record<string, string> | no | — | Environment variables; supports {{port}} and {{varName}} interpolation |
readyPattern | string | no | — | Regex matched against stdout/stderr; server is "ready" when matched |
readyTimeout | number (positive int) | no | 10000 | Milliseconds to wait for readyPattern before killing |
healthCheck | string | no | — | Reserved for future use. Accepted by schema but not used at runtime yet. |
singleton | boolean | no | true | Reserved for future use. Accepted by schema but not used at runtime yet (currently always one process per extension). |
cli config| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
command | string | yes | — | Executable to run |
args | string[] | no | [] | Arguments to command |
env | Record<string, string> | no | — | Environment variables |
Server extension:
{
"name": "my-server-ext",
"version": "0.1.0",
"label": "My Server Extension",
"description": "Does a thing with a server",
"category": "server",
"server": {
"command": "node",
"args": ["dist/index.js"],
"env": {
"PORT": "{{port}}"
},
"readyPattern": "listening on"
}
}
Client extension:
{
"name": "my-client-ext",
"version": "0.1.0",
"label": "My Client Extension",
"description": "A static HTML pane",
"category": "client",
"client": {
"entry": "index.html"
}
}
CLI extension:
{
"name": "my-cli-ext",
"version": "0.1.0",
"label": "My CLI Extension",
"description": "Wraps a TUI tool",
"category": "cli",
"cli": {
"command": "htop"
}
}
~/code/<name>).npm install (or equivalent for the project's stack).npm run build (or equivalent). Verify build artifacts exist (e.g., dist/).freshell.json in the project root.
server. Static HTML? → client. CLI tool? → cli.node dist/index.js)"listening on")category field, required fields present.mkdir -p ~/.freshell/extensions
ln -sf /absolute/path/to/extension ~/.freshell/extensions/<name>
Use absolute paths — relative symlinks break when the working directory changes.freshell.json — if missing, create one (see manifest reference above).mkdir -p ~/.freshell/extensions
ln -sf /absolute/path/to/project ~/.freshell/extensions/<name>
Create a directory with two files:
index.js:
const http = require('http');
const port = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello from my extension</h1>');
});
server.listen(port, () => console.log(`Listening on port ${port}`));
freshell.json:
{
"name": "hello-server",
"version": "0.1.0",
"label": "Hello Server",
"description": "Minimal server extension example",
"category": "server",
"server": {
"command": "node",
"args": ["index.js"],
"env": { "PORT": "{{port}}" },
"readyPattern": "Listening on"
}
}
No build step needed. Symlink and restart.
Create a directory with two files:
index.html:
<!DOCTYPE html>
<html>
<head><title>My Pane</title></head>
<body>
<h1>Hello from a client extension</h1>
<script>
// Your pane logic here
</script>
</body>
</html>
freshell.json:
{
"name": "hello-client",
"version": "0.1.0",
"label": "Hello Client",
"description": "Minimal client extension example",
"category": "client",
"client": {
"entry": "index.html"
}
}
No build step, no dependencies. Symlink and restart.
Just a manifest pointing at an existing binary. Single file:
freshell.json:
{
"name": "htop-pane",
"version": "0.1.0",
"label": "htop",
"description": "System monitor in a pane",
"category": "cli",
"cli": {
"command": "htop"
}
}
Create a directory with just this file, symlink, and restart.
Run through this before declaring an extension installed:
freshell.json is valid JSONname, version, label, description, category)readypattern vs readyPattern)category fieldcontentSchema defaults match their declared type (string default for type: "string", etc.)dist/), command is executable, readyPattern matches actual stdoutentry file exists at the specified pathls -la ~/.freshell/extensions/<name>)| Mistake | Symptom | Fix |
|---|---|---|
| Unknown key in manifest (typo or extra field) | Extension silently not loaded. Warning in server logs. | Remove the key. Only use keys listed in the manifest reference. |
| Multiple category config blocks | Validation fails, extension skipped | Remove extra blocks — only one of client/server/cli allowed |
Category block doesn't match category field | Validation fails | e.g., "category": "server" requires a "server": {...} block, not "client" |
| Build artifacts missing | Server extension fails to start (command can't find entry file) | Run the project's build step before symlinking |
| Relative symlink path | Symlink breaks when Freshell's cwd differs | Always use ln -sf /absolute/path |
| Missing PORT in server env | Server binds to wrong port; Freshell can't reach it | Add "PORT": "{{port}}" to server.env |
readyTimeout too low | Extension killed before it finishes starting | Increase readyTimeout (default is 10000ms) |
contentSchema default type mismatch | Validation fails (e.g., number default for type: "string") | Ensure typeof default === type |
| Expecting hot-reload after changes | Changes not picked up | Restart Freshell — extensions are scanned once at startup |
| Duplicate extension name | Second extension silently skipped (first wins) | Use unique names across all extension directories |
For reference, here's the manifest from the kilroy-run-pane extension (a server extension with contentSchema, url interpolation, and picker config):
{
"name": "kilroy-run-pane",
"version": "0.1.0",
"label": "Kilroy Run Viewer",
"description": "View Kilroy pipeline runs with DAG visualization and stage execution details",
"category": "server",
"server": {
"command": "node",
"args": ["dist-server/index.js"],
"env": {
"PORT": "{{port}}",
"KILROY_RUNS_DIR": "{{runsDir}}"
},
"readyPattern": "Listening on",
"readyTimeout": 10000,
"healthCheck": "/api/health",
"singleton": true
},
"url": "/run/{{runId}}",
"contentSchema": {
"runId": {
"type": "string",
"label": "Run ID",
"required": false
},
"runsDir": {
"type": "string",
"label": "Runs directory",
"default": "~/.local/state/kilroy/attractor/runs"
}
},
"picker": {
"shortcut": "R",
"group": "tools"
}
}
Note how {{runsDir}} in server.env is interpolated from the runsDir contentSchema default, and the ~/ prefix in that default is expanded to the user's home directory at runtime.
Use when preparing a Freshell release — before bumping versions, writing release notes, tagging, etc on GitHub.
Use when reviewing, triaging, or landing pull requests — especially batches of open PRs that need inspection, fixes, and merging. Also triages open issues after the PR queue is clear.
Use when interacting with Freshell panes, panels, or tabs from the CLI for tmux-style automation and multi-pane workflows, outside external-browser automation tasks.
Use when producing screen-recorded demos that need scenario-specific pane layouts, live interaction walkthroughs, and machine-readable timecodes for automated video editing.
Use when producing screen-recorded demos that need scenario-specific pane layouts, live interaction walkthroughs, and machine-readable timecodes for automated video editing.