Files
paseo/packages/cli/src/commands/plugin/scaffold.ts
T
Mohamed Boudra 929d2b717e fix(plugins): publish the plugin SDK under the @getpaseo scope
The daemon imports the plugin SDK at runtime, but the workspace was named
@paseo/plugin and marked private, so it was never published. 0.5.0-beta.1
shipped @getpaseo/server depending on a package that does not exist and
`npm install @getpaseo/cli@beta` failed with a 404 for every user.

Two things hid it. sync-workspace-versions only rewrites deps matching
@getpaseo/*, so the "*" range on @paseo/plugin survived untouched into the
published tarball. And `npm pack --dry-run`, which is all release:check and
CI run, lists files without ever resolving the dependency graph.

Renaming to @getpaseo/plugin rather than publishing the old name: @paseo is
not our scope on npm, so that name could never have been published at all.
The rename also brings the package under the version-sync filter, which is
what pins it correctly from here on.

Plugins scaffolded against the old name still import @paseo/plugin. Both
spellings resolve through plugin-sdk-specifiers.ts, tagged COMPAT with a
removal date, so existing plugins keep loading.

This does not repair 0.5.0-beta.1 — a published tarball's dependencies
cannot be changed. The package publishes with the next release.
2026-08-19 15:52:51 +02:00

325 lines
10 KiB
TypeScript

import { mkdir, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { PluginIdSchema } from "@getpaseo/protocol/messages";
const SDK_DECLARATIONS = `declare module "@getpaseo/plugin/server" {
import type { PaseoApi } from "@getpaseo/client";
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
export interface PluginRpcContract<
InputSchema extends ZodType = ZodType,
OutputSchema extends ZodType = ZodType,
> {
name: string;
input: InputSchema;
output: OutputSchema;
}
export interface PluginAttachmentItem {
id: string;
identifier: string;
title: string;
subtitle?: string;
url: string;
text: string;
resourceType: string;
}
export interface PluginAttachmentSearchPayload {
items: PluginAttachmentItem[];
}
export interface PluginAttachmentSourceContribution {
id: string;
title: string;
icon: string;
pickerTitle: string;
searchPlaceholder: string;
search: PluginRpcContract;
}
export interface PluginHandlerContext {
paseo: PaseoApi;
}
export function defineRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(definition: {
name: string;
input: InputSchema;
output: OutputSchema;
}): PluginRpcContract<InputSchema, OutputSchema>;
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
definition: Definition,
): Definition;
export const PluginAttachmentItemSchema: import("zod").ZodType<PluginAttachmentItem>;
export const PluginAttachmentSearchPayloadSchema: import("zod").ZodType<PluginAttachmentSearchPayload>;
}
declare module "@getpaseo/plugin" {
import type { ComponentType } from "react";
import type { PaseoApi } from "@getpaseo/client";
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
import type {
PluginAttachmentSourceContribution,
PluginHandlerContext,
PluginRpcContract,
} from "@getpaseo/plugin/server";
export {
PluginAttachmentItemSchema,
PluginAttachmentSearchPayloadSchema,
defineAttachmentSource,
defineRpc,
type PluginAttachmentItem,
type PluginAttachmentSearchPayload,
type PluginAttachmentSourceContribution,
type PluginHandlerContext,
type PluginRpcContract,
} from "@getpaseo/plugin/server";
export interface PluginTheme {
readonly colors: {
readonly surface0: string;
readonly foreground: string;
readonly foregroundMuted: string;
readonly accent: string;
readonly accentForeground: string;
readonly statusDanger: string;
};
}
export interface PluginHostProps {
theme: PluginTheme;
host: { id: string; label: string };
layout: { compact: boolean; platform: "ios" | "android" | "web" };
}
export interface PluginSurfaceProps extends PluginHostProps {}
export interface PluginWorkspaceSnapshot {
readonly id: string;
readonly projectId: string;
readonly projectDisplayName: string;
readonly projectRootPath: string;
readonly directory: string;
readonly projectKind: "git" | "non_git" | "directory";
readonly kind: "directory" | "local_checkout" | "checkout" | "worktree";
readonly name: string;
readonly title: string | null;
readonly status: "needs_input" | "failed" | "running" | "attention" | "done";
readonly statusEnteredAt: string | null;
readonly archivingAt: string | null;
readonly diffStat: { readonly additions: number; readonly deletions: number } | null;
}
export interface PluginAgentSnapshot {
readonly id: string;
readonly workspaceId: string;
readonly provider: string;
readonly status: "initializing" | "idle" | "running" | "error" | "closed";
readonly createdAt: string;
readonly updatedAt: string;
readonly lastActivityAt: string;
readonly title: string | null;
readonly cwd: string;
readonly model: string | null;
readonly currentModeId: string | null;
readonly thinkingOptionId: string | null;
readonly requiresAttention: boolean;
readonly attentionReason: "finished" | "error" | "permission" | null;
readonly parentAgentId: string | null;
readonly labels: Readonly<Record<string, string>>;
}
export interface PluginWorkspacePanelProps extends PluginHostProps {
context: "workspace";
workspaceId: string;
}
export interface PluginAgentPanelProps extends PluginHostProps {
context: "agent";
workspaceId: string;
agentId: string;
}
export type PluginWorkspacePanelContribution =
| { id: string; title: string; icon: string; context: "workspace"; Component: ComponentType<PluginWorkspacePanelProps> }
| { id: string; title: string; icon: string; context: "agent"; Component: ComponentType<PluginAgentPanelProps> };
export interface PluginSidebarContribution {
id: string;
title: string;
icon: string;
surface: string;
}
export interface PluginSurfaceContribution {
id: string;
Component: ComponentType<PluginSurfaceProps>;
}
export interface PluginCommandCapabilities {
paseo: PaseoApi;
rpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
contract: PluginRpcContract<InputSchema, OutputSchema>,
input: ZodInput<InputSchema>,
): Promise<ZodOutput<OutputSchema>>;
openSurface(id: string): void;
}
export interface PluginGlobalCommandContext extends PluginCommandCapabilities {
context: "global";
}
export interface PluginWorkspaceCommandContext extends PluginCommandCapabilities {
context: "workspace";
workspace: PluginWorkspaceSnapshot;
openPanel(id: string): void;
}
export interface PluginAgentCommandContext extends PluginCommandCapabilities {
context: "agent";
workspace: PluginWorkspaceSnapshot;
agent: PluginAgentSnapshot;
openPanel(id: string): void;
}
export type PluginCommandCenterItemContribution =
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "global"; onSelect(context: PluginGlobalCommandContext): void | Promise<void> }
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "workspace"; onSelect(context: PluginWorkspaceCommandContext): void | Promise<void> }
| { id: string; title: string; icon: string; keywords?: readonly string[]; context: "agent"; onSelect(context: PluginAgentCommandContext): void | Promise<void> };
export interface PluginContext {
handle<InputSchema extends ZodType, OutputSchema extends ZodType>(
contract: PluginRpcContract<InputSchema, OutputSchema>,
handler: (
input: ZodOutput<InputSchema>,
context: PluginHandlerContext,
) => ZodInput<OutputSchema> | Promise<ZodInput<OutputSchema>>,
): void;
addSurface(id: string, Component: ComponentType<PluginSurfaceProps>): void;
addSidebarItem(contribution: PluginSidebarContribution): void;
addWorkspacePanel(contribution: PluginWorkspacePanelContribution): void;
addCommandCenterItem(contribution: PluginCommandCenterItemContribution): void;
addAttachmentSource(contribution: PluginAttachmentSourceContribution): void;
}
export type PluginCleanup = () => void | Promise<void>;
export type PluginContribution = (plugin: PluginContext) => PluginCleanup;
export function useRpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
contract: PluginRpcContract<InputSchema, OutputSchema>,
): (input: ZodInput<InputSchema>) => Promise<ZodOutput<OutputSchema>>;
export function usePaseo(): PaseoApi;
export function useWorkspace<Selection>(
workspaceId: string,
selector: (workspace: PluginWorkspaceSnapshot) => Selection,
): Selection | null;
export function useAgent<Selection>(
agentId: string,
selector: (agent: PluginAgentSnapshot) => Selection,
): Selection | null;
}
`;
const TSCONFIG = {
compilerOptions: {
target: "ES2020",
module: "ESNext",
moduleResolution: "Bundler",
lib: ["ES2023", "DOM"],
jsx: "react-jsx",
strict: true,
skipLibCheck: true,
noEmit: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
include: ["**/*.ts", "**/*.tsx"],
};
const ENTRY = `import type { PluginContext } from "@getpaseo/plugin";
import { MainSurface } from "./main.client";
export default function contribute(plugin: PluginContext) {
plugin.addSurface("main", MainSurface);
return () => {};
}
`;
const CLIENT_SURFACE = `import type { PluginSurfaceProps } from "@getpaseo/plugin";
import React, { useMemo } from "react";
import { Text, View } from "react-native";
export function MainSurface({ theme, layout }: PluginSurfaceProps) {
const styles = useMemo(
() => ({
screen: {
flex: 1,
padding: layout.compact ? 16 : 24,
backgroundColor: theme.colors.surface0,
},
text: { color: theme.colors.foreground },
}),
[theme, layout.compact],
);
return (
<View style={styles.screen}>
<Text style={styles.text}>Hello from my plugin</Text>
</View>
);
}
`;
export interface PluginScaffold {
id: string;
directory: string;
}
export async function scaffoldPluginDirectory(
targetDirectory: string,
requestedId?: string,
): Promise<PluginScaffold> {
const directory = path.resolve(targetDirectory);
const id = PluginIdSchema.parse(requestedId ?? path.basename(directory));
await mkdir(directory, { recursive: true });
const existing = await readdir(directory);
if (existing.length > 0) {
throw new Error(`Plugin directory must be empty: ${directory}`);
}
const packageJson = {
name: id,
private: true,
version: "0.0.0",
scripts: { typecheck: "tsc --noEmit" },
devDependencies: {
"@getpaseo/client": "^0.4.0",
"@tanstack/react-query": "^5.90.11",
"@types/react": "~19.2.0",
react: "19.1.0",
"react-native": "0.81.5",
typescript: "^5.9.3",
zod: "^4.4.3",
},
};
const files = new Map<string, string>([
["paseo-plugin.json", `${JSON.stringify({ id }, null, 2)}\n`],
["package.json", `${JSON.stringify(packageJson, null, 2)}\n`],
["tsconfig.json", `${JSON.stringify(TSCONFIG, null, 2)}\n`],
["paseo-plugin.d.ts", SDK_DECLARATIONS],
["index.ts", ENTRY],
["main.client.tsx", CLIENT_SURFACE],
]);
await Promise.all(
[...files].map(([filename, contents]) =>
writeFile(path.join(directory, filename), contents, { flag: "wx" }),
),
);
return { id, directory };
}