diff --git a/.agents/skills/new-extension/SKILL.md b/.agents/skills/new-extension/SKILL.md new file mode 100644 index 00000000000..ca77c919104 --- /dev/null +++ b/.agents/skills/new-extension/SKILL.md @@ -0,0 +1,564 @@ +--- +name: new-extension +description: >- + Scaffold a new Podman Desktop extension as a standalone repository with all + required boilerplate, build config, Containerfile, and Svelte webview. Use + when the user asks to create a new extension, add extension boilerplate, + scaffold an extension, or bootstrap a Podman Desktop extension project. +--- + +# Create a New Podman Desktop Extension + +Scaffold a standalone extension in its own repository. The user provides an extension name and a brief description of what it should do. You produce all required files and verify the extension builds and can be loaded locally into Podman Desktop. + +## Inputs + +Ask the user (or infer from context): + +1. **Extension name** — kebab-case identifier (e.g. `apple-container`). Used as the directory/repo name and the `name` field in `package.json`. +2. **Display name** — human-readable title (e.g. `Apple Container`). +3. **Description** — one-sentence summary shown in the extension list. +4. **Feature scope** — what the extension should do. Common patterns: + - **Minimal** — register a command, show a notification + - **Webview** — display a Svelte UI panel (this is the default for any extension that shows content) + - **Provider** — register a container engine or Kubernetes provider + - **Status bar / Tray** — add status bar entries or tray menu items + - **Configuration** — add extension-specific settings + - Combinations of the above +5. **Publisher** — the org or username that owns the extension (default: the user's GitHub username). + +If the extension needs to display any UI beyond simple notifications, **always use the multi-package webview layout** (not inline HTML). This is the standard for all production Podman Desktop extensions. + +--- + +## Where to look in the Podman Desktop codebase + +When implementing extension features, you may need to look up API details: + +| What you need | Where to look | +| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| Full extension API types (`createWebviewPanel`, `containerEngine`, `provider`, etc.) | `packages/extension-api/src/extension-api.d.ts` in the podman-desktop repo | +| Extension manifest schema (what `contributes` fields exist) | `packages/main/src/plugin/extension/extension-manifest-schema.ts` | +| How the extension loader discovers and activates extensions | `packages/main/src/plugin/extension/extension-loader.ts` | +| Container operations (`createContainer`, `startContainer`, `pullImage`, `listContainers`) | Search for `export namespace containerEngine` in `extension-api.d.ts` | +| Provider operations (`createProvider`, `getContainerConnections`) | Search for `export namespace provider` in `extension-api.d.ts` | +| Webview types (`WebviewPanel`, `Webview`, `WebviewOptions`) | Search for `interface WebviewPanel` in `extension-api.d.ts` | +| Container create options (`Image`, `Cmd`, `HostConfig`, `PortBindings`, `ExposedPorts`) | Search for `interface ContainerCreateOptions` in `extension-api.d.ts` | +| Existing built-in extensions as examples | `extensions/` directory (e.g. `extensions/registries/`, `extensions/kube-context/`) | +| How extensions are published to OCI registries | `website/docs/extensions/publish/index.md` | + +--- + +## Step 1 — Choose layout + +### Minimal (no UI) + +For extensions that only register commands, providers, status bar items, or configuration — no webview needed. + +``` +{name}/ +├── .gitignore +├── Containerfile +├── LICENSE +├── README.md +├── icon.png +├── package.json +├── tsconfig.json +└── src/ + └── extension.ts +``` + +### Multi-package with Svelte webview (default for any UI) + +**Always use this layout when the extension displays content.** Do not use inline HTML strings. The frontend is a Svelte app built by Vite into static assets that the backend loads into a webview panel. + +``` +{name}/ +├── .gitignore +├── Containerfile +├── LICENSE +├── README.md +├── package.json # root workspace — runs both packages +├── packages/ +│ ├── backend/ # the extension entry point (Node.js) +│ │ ├── icon.png +│ │ ├── package.json # has "main": "./dist/extension.js" +│ │ ├── tsconfig.json +│ │ ├── vite.config.ts +│ │ └── src/ +│ │ └── extension.ts +│ ├── frontend/ # Svelte app built into backend/media/ +│ │ ├── index.html +│ │ ├── package.json +│ │ ├── svelte.config.js +│ │ ├── tsconfig.json +│ │ ├── vite.config.ts +│ │ └── src/ +│ │ ├── main.ts +│ │ └── App.svelte +│ └── shared/ # (optional) shared types between frontend & backend +│ └── src/ +│ └── ... +``` + +--- + +## Step 2 — File contents (multi-package webview) + +### .gitignore + +``` +dist/ +media/ +node_modules/ +``` + +`media/` is a build artifact (frontend output) — do not commit it. + +### Root package.json + +```json +{ + "name": "{name}", + "displayName": "{displayName}", + "description": "{description}", + "version": "0.0.1", + "private": true, + "engines": { "node": ">=22.0.0", "npm": ">=11.0.0" }, + "scripts": { + "build": "concurrently \"npm run -w packages/frontend build\" \"npm run -w packages/backend build\"", + "watch": "concurrently \"npm run -w packages/frontend watch\" \"npm run -w packages/backend watch\"" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "typescript": "^5.9.3", + "vite": "^7.0.0" + }, + "workspaces": ["packages/*"] +} +``` + +> **Vite version constraint:** `@sveltejs/vite-plugin-svelte@6.x` requires Vite 6 or 7. Do not use Vite 8+. + +### packages/backend/package.json + +This is the **extension manifest** that Podman Desktop reads: + +```json +{ + "name": "{name}", + "displayName": "{displayName}", + "description": "{description}", + "version": "0.0.1", + "icon": "icon.png", + "publisher": "{publisher}", + "license": "Apache-2.0", + "engines": { "podman-desktop": ">=1.26.0" }, + "main": "./dist/extension.js", + "contributes": {}, + "scripts": { + "build": "vite build", + "watch": "vite --mode development build -w" + }, + "devDependencies": { + "@podman-desktop/api": "^1.26.1", + "@types/node": "^22" + } +} +``` + +### packages/backend/tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "outDir": "dist", + "skipLibCheck": true, + "types": ["node"], + "strict": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] +} +``` + +### packages/backend/vite.config.ts + +Use `.ts` for type-safe config. The backend builds as a CJS library entry point with `@podman-desktop/api` and all Node builtins externalized. + +```ts +import { join } from 'path'; +import { builtinModules } from 'module'; +import { defineConfig } from 'vite'; + +const PACKAGE_ROOT = __dirname; + +export default defineConfig({ + mode: process.env.MODE, + root: PACKAGE_ROOT, + envDir: process.cwd(), + resolve: { + alias: { + '/@/': join(PACKAGE_ROOT, 'src') + '/', + }, + }, + build: { + sourcemap: 'inline', + target: 'esnext', + outDir: 'dist', + assetsDir: '.', + minify: process.env.MODE === 'production' ? 'esbuild' : false, + lib: { + entry: 'src/extension.ts', + formats: ['cjs'], + }, + rollupOptions: { + external: ['@podman-desktop/api', ...builtinModules.flatMap(p => [p, `node:${p}`])], + output: { entryFileNames: '[name].js' }, + }, + emptyOutDir: true, + reportCompressedSize: false, + }, +}); +``` + +### packages/frontend/package.json + +```json +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "watch": "vite --mode development build -w" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.1.0", + "svelte": "^5.53.5" + } +} +``` + +Optionally add `@podman-desktop/ui-svelte` for native Podman Desktop components (Button, EmptyScreen, etc.), and `tailwindcss` + `autoprefixer` + `postcss` if you want Tailwind styling. + +### packages/frontend/tsconfig.json + +The frontend runs in a browser-like webview environment, so it needs DOM libs and bundler resolution: + +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "verbatimModuleSyntax": true + }, + "include": ["src"] +} +``` + +### packages/frontend/svelte.config.js + +Required for Svelte preprocessing (TypeScript in `.svelte` files): + +```js +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: [vitePreprocess()], +}; +``` + +### packages/frontend/vite.config.ts + +The key: `outDir: '../backend/media'` — Vite builds the Svelte app into the backend's `media/` folder. + +```ts +import { join } from 'path'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { defineConfig } from 'vite'; +import { fileURLToPath } from 'url'; +import path from 'path'; + +const filename = fileURLToPath(import.meta.url); +const PACKAGE_ROOT = path.dirname(filename); + +export default defineConfig({ + mode: process.env.MODE, + root: PACKAGE_ROOT, + resolve: { + alias: { '/@/': join(PACKAGE_ROOT, 'src') + '/' }, + }, + plugins: [svelte()], + base: '', + build: { + sourcemap: true, + outDir: '../backend/media', + assetsDir: '.', + emptyOutDir: true, + reportCompressedSize: false, + }, +}); +``` + +### packages/frontend/index.html + +```html + + +
+ + +