diff --git a/packages/cli/src/commands/plugin/scaffold.test.ts b/packages/cli/src/commands/plugin/scaffold.test.ts index df0c13295b..c69ca4c197 100644 --- a/packages/cli/src/commands/plugin/scaffold.test.ts +++ b/packages/cli/src/commands/plugin/scaffold.test.ts @@ -1,4 +1,4 @@ -import { execFile } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; @@ -9,6 +9,7 @@ import { scaffoldPluginDirectory } from "./scaffold.js"; const directories: string[] = []; const execFileAsync = promisify(execFile); +const execAsync = promisify(exec); const tscPath = createRequire(import.meta.url).resolve("typescript/bin/tsc"); async function typecheckPlugin(directory: string): Promise { @@ -28,6 +29,47 @@ afterEach(async () => { }); describe("plugin scaffold", () => { + it("creates an author overview beside the manifest", async () => { + const parent = await mkdtemp(path.join(tmpdir(), "paseo-plugin-scaffold-")); + directories.push(parent); + const directory = path.join(parent, "hello-plugin"); + await scaffoldPluginDirectory(directory); + + const overview = await readFile(path.join(directory, "OVERVIEW.md"), "utf8"); + const comments = overview.match(//g) ?? []; + expect(comments).toHaveLength(1); + expect(overview.replace(//g, "").trim()).toBe(""); + expect(overview).toContain("required to list"); + expect(overview).toContain("README"); + expect(overview).toContain("choosing a plugin in Paseo"); + expect(overview).toContain("Length follows complexity"); + expect(overview).toContain("1. Describe what your plugin is and does"); + expect(overview).toContain("2. Explain how it works only when it is not obvious"); + expect(overview).toContain("3. Explain setup when needed"); + expect(overview).toContain("4. Explain capabilities and settings"); + expect(overview).toContain("permissions"); + expect(overview).toContain("known limits"); + expect(overview).not.toContain("This plugin entry was imported"); + const packageJson = JSON.parse(await readFile(path.join(directory, "package.json"), "utf8")); + expect(packageJson.files).toContain("OVERVIEW.md"); + expect(await readdir(directory)).toContain("paseo-plugin.json"); + }); + + it("includes the author overview in the packed npm artifact", async () => { + const parent = await mkdtemp(path.join(tmpdir(), "paseo-plugin-scaffold-")); + directories.push(parent); + const directory = path.join(parent, "packed-plugin"); + await scaffoldPluginDirectory(directory); + + const { stdout } = await execAsync("npm pack --dry-run --ignore-scripts --json", { + cwd: directory, + }); + const [artifact] = JSON.parse(stdout) as { files: { path: string }[] }[]; + expect(artifact.files.map((file) => file.path)).toEqual( + expect.arrayContaining(["OVERVIEW.md", "paseo-plugin.json"]), + ); + }); + it( "creates a standalone split-runtime project that typechecks", { timeout: 15_000 }, @@ -60,6 +102,7 @@ describe("plugin scaffold", () => { version: "0.0.0", files: [ "paseo-plugin.json", + "OVERVIEW.md", "index.client.ts", "index.client.tsx", "index.server.ts", diff --git a/packages/cli/src/commands/plugin/scaffold.ts b/packages/cli/src/commands/plugin/scaffold.ts index 448aa956fa..f0f1fe3b93 100644 --- a/packages/cli/src/commands/plugin/scaffold.ts +++ b/packages/cli/src/commands/plugin/scaffold.ts @@ -3,6 +3,34 @@ import path from "node:path"; import { PluginIdSchema } from "@getpaseo/protocol/messages"; import { resolveCliVersion } from "../../version.js"; +const OVERVIEW = ` +`; + const TSCONFIG = { compilerOptions: { target: "ES2020", @@ -155,6 +183,7 @@ export async function scaffoldPluginDirectory( version: "0.0.0", files: [ "paseo-plugin.json", + "OVERVIEW.md", "index.client.ts", "index.client.tsx", "index.server.ts", @@ -179,6 +208,7 @@ export async function scaffoldPluginDirectory( "paseo-plugin.json", `${JSON.stringify({ id, requirements: { paseo: `>=${version}` } }, null, 2)}\n`, ], + ["OVERVIEW.md", OVERVIEW], ["package.json", `${JSON.stringify(packageJson, null, 2)}\n`], ["tsconfig.json", `${JSON.stringify(TSCONFIG, null, 2)}\n`], ["index.client.tsx", CLIENT_ENTRY], diff --git a/public-docs/plugins/publishing.md b/public-docs/plugins/publishing.md index 0e7f2f89cf..a021bda644 100644 --- a/public-docs/plugins/publishing.md +++ b/public-docs/plugins/publishing.md @@ -56,6 +56,7 @@ The scaffold includes this `files` list in `package.json`: { "files": [ "paseo-plugin.json", + "OVERVIEW.md", "index.client.ts", "index.client.tsx", "index.server.ts", @@ -136,6 +137,46 @@ In the app, enter only the source identifier. ::: +## Your listing page + +Write `OVERVIEW.md` beside `paseo-plugin.json` for the plugin page inside Paseo. +It helps someone decide whether to install your plugin; the install command is already at +the top of that page. A README assumes a GitHub audience and carries installation +instructions, technical details, and badges. Long, AI-generated READMEs make people read +past that material to understand what a plugin does. + +`OVERVIEW.md` is required to list your plugin in the registry. Commit it beside +`paseo-plugin.json` in the source repository at the pinned commit. The registry resolves it +relative to the manifest, under `pluginPath` for monorepos. Include it in the published npm +package too; the scaffold's `files` list includes it. Replace the scaffold's guidance comment +with useful facts before publishing. + +The repository overview takes precedence over a registry import stopgap. Online validation +fails when the pinned commit has no `OVERVIEW.md`, except for unchanged imported records +that already carry `plugins//.md` in the registry repository. Every version bump +requires a repository overview and removes the stopgap in the same PR. A bump without the +repository overview fails validation. Neither README files nor a `paseo-listing.json` +`readme` override supplies the overview. + +Author overviews and registry stopgaps follow the same content contract, in this order. +Choose headings only when they help; length follows complexity. A theme needs one paragraph. + +1. Explain what the plugin is and does in plain terms first. +2. Explain how it works only when that is not obvious. +3. Explain setup when needed: settings, accounts, tokens, providers, external tools, or other + plugins. Include applicable daemon version and operating system requirements. Setup + guidance is allowed; installation instructions are not. +4. Explain capabilities and settings worth understanding, what each option does, what the + plugin reads or sends and where, permissions, and known limits. + +Use sentence case and plain factual language, with no em dashes. Omit installation commands, +badges, changelog, contributing or license sections, marketing, and unsupported claims. +Avoid implementation filler such as empty cleanup functions, catalogs of theme-token fields, +or lists of absent features. Keep only what helps someone choose the plugin. + +If your plugin is an unchanged imported record, you can propose an author-written overview +to replace the registry stopgap. Do not add import credit to your own `OVERVIEW.md`. + ## Share through GitHub or Git Push the plugin project to a repository. Users can install it with: diff --git a/skills/paseo-plugin/SKILL.md b/skills/paseo-plugin/SKILL.md index 6fc8c90f87..f229bf7a3a 100644 --- a/skills/paseo-plugin/SKILL.md +++ b/skills/paseo-plugin/SKILL.md @@ -64,6 +64,7 @@ The generated project contains: ```text my-plugin/ paseo-plugin.json + OVERVIEW.md package.json tsconfig.json index.client.tsx @@ -638,6 +639,48 @@ Do not edit a local config when the target is a remote daemon. Perform the edit When the same screen or sidebar item exists on several connected hosts, Paseo shows it once. The screen header has a host picker; a sidebar item uses the host of the screen on display, else the host last picked in one of the plugin's screens. The selected host owns the bundle, SDK calls, RPCs, and query cache. An offline selected host does not fall through to another host. Attachment sources stay scoped to the composer's host. +## Publishing a listing + +Write `OVERVIEW.md` beside `paseo-plugin.json` for the plugin page inside Paseo. +It helps someone decide whether to install your plugin; the install command is already at +the top of that page. A README assumes a GitHub audience and carries installation +instructions, technical details, and badges. Long, AI-generated READMEs make people read +past that material to understand what a plugin does. + +`OVERVIEW.md` is required to list your plugin in the registry. Commit it beside +`paseo-plugin.json` in the source repository at the pinned commit. The registry resolves it +relative to the manifest, under `pluginPath` for monorepos. Include it in the published npm +package too; the scaffold's `files` list includes it. Replace the scaffold's guidance comment +with useful facts before publishing. + +The repository overview takes precedence over a registry import stopgap. Online validation +fails when the pinned commit has no `OVERVIEW.md`, except for unchanged imported records +that already carry `plugins//.md` in the registry repository. Every version bump +requires a repository overview and removes the stopgap in the same PR. A bump without the +repository overview fails validation. Neither README files nor a `paseo-listing.json` +`readme` override supplies the overview. + +Author overviews and registry stopgaps follow the same content contract, in this order. +Choose headings only when they help; length follows complexity. A theme needs one paragraph. + +1. Explain what the plugin is and does in plain terms first. +2. Explain how it works only when that is not obvious. +3. Explain setup when needed: settings, accounts, tokens, providers, external tools, or other + plugins. Include applicable daemon version and operating system requirements. Setup + guidance is allowed; installation instructions are not. +4. Explain capabilities and settings worth understanding, what each option does, what the + plugin reads or sends and where, permissions, and known limits. + +Use sentence case and plain factual language, with no em dashes. Omit installation commands, +badges, changelog, contributing or license sections, marketing, and unsupported claims. +Avoid implementation filler such as empty cleanup functions, catalogs of theme-token fields, +or lists of absent features. Keep only what helps someone choose the plugin. + +If your plugin is an unchanged imported record, you can propose an author-written overview +to replace the registry stopgap. Do not add import credit to your own `OVERVIEW.md`. + +See [Your listing page](https://paseo.sh/docs/plugins/publishing.md#your-listing-page). + ## Typecheck and manage When editing a plugin, typecheck its source before install or reload: