agentleFS
Surething-ioSurething-io/cockpit/skills/html/SKILL.md

html

Skill44 starsChanged 8 months agoMITView on GitHub

Generate an interactive local React app wired to the built-in cockpit bash SDK.

  • Sends data out

18 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use thermo-nuclear-code-quality-review, prototype and 4 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • jaydubya818Skill

    thermo-nuclear-code-quality-review

    Use when the user requests an intentionally strict maintainability review.

    Also in 3 projects that use this

  • GoogleSkill

    prototype

    Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

    Also in 4 projects that use this

  • GoogleSkill

    to-spec

    Turn the current conversation into a spec and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.

    Also in 3 projects that use this

  • MicrosoftSkill

    handoff

    Compact the current conversation into a handoff document for another agent to pick up.

    Also in 4 projects that use this

  • +2 more from google

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 17 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every file of the app lives in that one directory, with data split into cache/ state/ out/ per section 4.

    line 29
  2. Very specific:

    All four meta lines matter: the HTML panel renders cards from them, and the console opens the app via cockpit-name (/name).

    line 73
  3. Very specific:

    When one command is enough and the data isn't worth keeping (just viewing current state), skip api.mjs and call cockpit.bash("curl -s …") …

    line 159

“Common” is measured: kept by at least 6 in 10 of the 17 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Build a Cockpit app (a local React page)

A local application, not a one-off page: the preview injects a global window.cockpit SDK — essentially the Bash tool exposed to the page, so buttons can curl, read/write files and run scripts; data lands on disk inside the app directory and is there on reopen.

Zero build: React / Babel load from the same-origin /html-lib — no bundler, no deps, works offline.

1. Flow: align first, write only after confirmation (important)

Don't write files up front. Three steps:

  1. Align on the requirement — restate your understanding of the app (what it does, where data comes from, key features/interactions). Ask back only when the requirement is ambiguous or has several reasonable readings; if it's already clear, just give a short summary — don't interrogate.
  2. Tell the storage directory — resolve <dir>/<name>/ and tell the user exactly where it goes and what it's called; they may reject or change it:
    • You always pick <name> from the request (lowercase kebab-case, matching cockpit-name) — the user only describes the app, they don't name it.
    • The user gave a directory → <the given directory>/<name>/; none given → <name>/ under the current chat working directory (this session's cwd).
    • Every file of the app lives in that one directory, with data split into cache/ state/ out/ per section 4. Don't scatter files elsewhere or invent a .cockpit-apps.
  3. Write only after confirmation — Write the files once the user clearly agrees (confirm / start / go / "write it" — any affirmative). Until then, only discuss.

Even if the user already spelled out requirement and directory in the /html call, still stop once to present "understanding + directory + a 'shall I start writing?'" and wait — but keep that round lightweight, don't re-interrogate.

2. Skeleton: three files, copy them

index.html — use this fixed shell, don't improvise:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Page title</title>
  <meta name="cockpit-name" content="short-name">   <!-- unique short name for /name; letters/digits/-_ -->
  <meta name="description" content="one line about the page">
  <meta name="cockpit-icon" content="🔍">           <!-- emoji or icon url, optional -->
  <meta name="cockpit-theme" content="auto">        <!-- light/dark toggle, see section 5 -->
  <link rel="stylesheet" href="/html-lib/theme.css">
</head>
<body>
  <div id="root"></div>
  <script src="/html-lib/react.production.min.js"></script>
  <script src="/html-lib/react-dom.production.min.js"></script>
  <script src="/html-lib/babel.min.js"></script>
  <script>
    (async () => {
      const src = await (await fetch('./app.jsx')).text();
      // MUST use classic runtime: the default automatic runtime injects an import of
      // react/jsx-runtime, which — appended as a plain script — throws
      // "Cannot use import statement outside a module".
      const { code } = Babel.transform(src, { presets: [['react', { runtime: 'classic' }]] });
      const s = document.createElement('script'); s.textContent = code; document.body.appendChild(s);
    })();
  </script>
</body>
</html>

All four meta lines matter: the HTML panel renders cards from them, and the console opens the app via cockpit-name (/name).

app.jsx — global React, render at the end. Opening reads the cache; only Refresh goes out:

const { useState, useEffect, useCallback } = React;

function App() {
  const [data, setData] = useState(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  const load = useCallback(async (refresh) => {
    setBusy(true); setErr(null);
    try {
      const arg = btoa(JSON.stringify({ refresh: !!refresh }));      // structured args as base64
      const { stdout, stderr, exitCode } = await cockpit.bash(`node ./api.mjs ${arg}`);
      if (exitCode !== 0) throw new Error(stderr || 'script failed');  // ran, but failed
      const d = JSON.parse(stdout);
      if (d.error && !d.stale) throw new Error(d.error);
      setData(d);
      if (d.stale) setErr(d.error);                                  // old data is still shown
    } catch (e) {
      setErr(e.message);      // a throw = spawn/infra failure. Keep existing data on screen
    } finally { setBusy(false); }
  }, []);

  useEffect(() => { load(false); }, [load]);       // on open: cache only, zero requests

  if (!data) return <div>{err ?? 'Loading…'}</div>;
  return (
    <div>
      <span>{data.cached ? 'cached' : 'updated'} · {new Date(data.fetchedAt).toLocaleString()}</span>
      <button disabled={busy} onClick={() => load(true)}>{busy ? 'Refreshing…' : 'Refresh'}</button>
      {err && <em>Refresh failed, showing older data: {err}</em>}
      {/* render data */}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

api.mjs — the backend handler: fetching, parsing and persistence live here; one JSON on stdout:

import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const DIR = dirname(fileURLToPath(import.meta.url));
const CACHE = join(DIR, 'cache/data.json');
const VERSION = 1;          // bump whenever the payload shape changes, or old cache feeds a new UI

const args = JSON.parse(Buffer.from(process.argv[2] || '', 'base64').toString() || '{}');

function readCache() {
  if (!existsSync(CACHE)) return null;
  const c = JSON.parse(readFileSync(CACHE, 'utf8'));
  return c.v === VERSION ? c : null;          // version mismatch = void it, act as if empty
}
function writeCache(payload) {
  mkdirSync(dirname(CACHE), { recursive: true });
  const tmp = `${CACHE}.${process.pid}.tmp`;   // atomic: a kill mid-write leaves no half JSON
  writeFileSync(tmp, JSON.stringify({ v: VERSION, fetchedAt: new Date().toISOString(), payload }));
  renameSync(tmp, CACHE);
}

try {
  const hit = args.refresh ? null : readCache();
  if (hit) {
    process.stdout.write(JSON.stringify({ ...hit.payload, cached: true, fetchedAt: hit.fetchedAt }));
  } else {
    const payload = await fetchFresh(args);   // ← your fetching logic (curl / DB / commands)
    writeCache(payload);
    process.stdout.write(JSON.stringify({ ...payload, cached: false, fetchedAt: new Date().toISOString() }));
  }
} catch (e) {
  const hit = readCache();                    // failed refresh: fall back to the old cache, labelled
  process.stdout.write(JSON.stringify(hit
    ? { ...hit.payload, cached: true, stale: true, error: e.message, fetchedAt: hit.fetchedAt }
    : { error: e.message }));
}
  • When one command is enough and the data isn't worth keeping (just viewing current state), skip api.mjs and call cockpit.bash("curl -s …") directly. The moment data should survive a reopen, use this skeleton rather than inventing your own.
  • Same for other languages: python3 ./api.py / bash ./api.sh (explicit interpreter avoids chmod +x); probe node / python3 with command -v if unsure and fall back to shell.
  • The cache key must include every parameter that changes the result (time range, fetch depth …): use cache/<param>.json or bucket by key inside the file — otherwise a 7-day cache renders in the 30-day view.

3. Contract: window.cockpit

The SDK is ready on load — no library to import:

  • cockpit.cwd: string — directory of the current file; relative-path commands run here.
  • cockpit.bash(command, opts?) — run one bash command (command is a raw shell string):
    • Foreground (default, short/discrete) → Promise<{ stdout, stderr, exitCode }>; await for the full output at once.
    • Background (opts.background: true, long/live) → { kill() }, streaming via callbacks. Large/continuous output must use background — foreground buffers it all in memory.
const h = cockpit.bash("npm run build", {
  background: true,
  onOutput: c => append(c),
  onStderr: c => append(c),
  onExit:   code => append('\n[exit ' + code + ']'),   // command finished; non-zero = failed
  onError:  msg  => append('\n[error ' + msg + ']'),   // spawn / infra failure
});
// h.kill() on unmount or when the user aborts

Rules every page must follow:

  1. Data only through cockpit.bash (curl to read, curl -X POST to write, or read/write files) — never fetch(externalURL): the same-origin sandbox blocks it via CORS. (fetch('./app.jsx') and other same-directory relative requests are fine.)
  2. Handle both failure modes: a throw caught by try/catch = spawn/infra failure (bad path, dropped connection); no throw but exitCode !== 0 = the command ran and failed — show stderr or the error body in stdout, don't just say "failed". Background: onError (infra) + a non-zero code in onExit (command).
  3. command is a shell string: validate or escape any dynamic/user input before interpolating it (shell metacharacters inject); pass structured input as base64 (see the skeleton) instead of string-building; use parameterized statements for DB writes.
  4. Resource refs: relative siblings (./app.jsx, ./style.css, images) and absolute CDN URLs load; root-relative (/assets/x.css) 404s — the server only hosts /html-lib/* and /apps/*.

4. Local data: three directories

Data must land on disk and be there on reopen — don't refetch everything on every open. All persistence goes through api.mjs; inside the app directory, split it by "can this be rebuilt?" into three directories instead of one flat pile:

  • cache/ — rebuildable (API responses, raw dumps, derived indexes). Deleting it only costs time. Gitignore the whole directory.
  • state/ — NOT rebuildable (daily snapshots, hand-maintained lists). The source can't hand it to you twice; deleted means gone. Commit it.
  • out/ — artifacts for humans (reports, exports), date-named, kept.

The split only pays off through the git policy: directories separated but cache/ not ignored and state/ not committed means nothing was separated.

What the skeleton already handles (versioning, atomic writes, the refresh flag, the fetch timestamp, stale fallback) isn't repeated here. What's left is on you:

  1. Any growing directory needs a cap enforced in the script (keep N entries / prune past X MB). A cap that lives only in a comment is not a cap — unattended dump directories reach GB scale.
  2. Never write absolute paths into a file on disk (store paths relative to the app directory), or moving the directory voids the index; anything holding private data (emails …) is gitignored.
  3. Provide a clear/reset entry point (a clear_cache mode in api.mjs + a button), or the user's only option is guessing which file is safe to delete in a file manager.
  4. To use another app's credentials / helper scripts, reference them as ../<app>/ and probe that they exist first; on failure give one actionable line ("log in again at xxx") rather than dumping the raw error.

5. Theme

Style with the Cockpit theme by default (index.html already links /html-lib/theme.css). Fetch that file once before you pick any colour — it is the single source of truth for the variables, so don't guess names from memory:

curl -fsS "{{BASE_URL}}/html-lib/theme.css"
  • :root holds the light values, .dark the dark ones. The raw scales (--slate-* / --teal-* / --red-* …) store HSL components, so translucency is just hsl(var(--green-9) / .12).
  • Take foregrounds in pairs (--card/--card-foreground …), except --brand: --brand-foreground is a tint of the same hue and hits only 1.5:1 on a --brand fill — use var(--background) for text on a brand fill.
  • The cockpit-theme meta gives the preview a floating light/dark toggle (top-right by default, draggable to any corner — so don't reserve top-right space for it); it follows the Cockpit host theme by default (outside Cockpit, the meta's auto/light/dark decides), and a manual toggle is remembered per app across reloads and overrides the host, or call cockpit.toggleTheme().
  • Don't invent a palette or add Tailwind; add a small <style> for app-specific bits.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.