mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Add plugin init CLI scaffold command
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0808b388ee
commit
449fdca730
4 files changed
+218
-2
No files matched your search
@@ -47,6 +47,7 @@
|
||||
"@paperclipai/adapter-pi-local": "workspace:*",
|
||||
"@paperclipai/adapter-openclaw-gateway": "workspace:*",
|
||||
"@paperclipai/adapter-utils": "workspace:*",
|
||||
"@paperclipai/create-paperclip-plugin": "workspace:*",
|
||||
"@paperclipai/db": "workspace:*",
|
||||
"@paperclipai/server": "workspace:*",
|
||||
"@paperclipai/shared": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
scaffoldPluginProject: vi.fn((options: { outputDir: string }) => options.outputDir),
|
||||
}));
|
||||
|
||||
vi.mock("@paperclipai/create-paperclip-plugin", () => ({
|
||||
scaffoldPluginProject: mocks.scaffoldPluginProject,
|
||||
}));
|
||||
|
||||
import {
|
||||
buildPluginInitNextCommands,
|
||||
buildPluginInitScaffoldOptions,
|
||||
registerPluginCommands,
|
||||
} from "../commands/client/plugin.js";
|
||||
|
||||
describe("plugin init", () => {
|
||||
beforeEach(() => {
|
||||
mocks.scaffoldPluginProject.mockClear();
|
||||
});
|
||||
|
||||
it("maps package name and flags to scaffolder options", () => {
|
||||
const cwd = path.resolve("/tmp/paperclip-cli-test");
|
||||
const options = buildPluginInitScaffoldOptions(
|
||||
"@acme/plugin-linear",
|
||||
{
|
||||
output: "plugins",
|
||||
template: "connector",
|
||||
category: "automation",
|
||||
displayName: "Linear Bridge",
|
||||
description: "Syncs Linear issues",
|
||||
author: "Acme",
|
||||
sdkPath: "../paperclip/packages/plugins/sdk",
|
||||
},
|
||||
cwd,
|
||||
);
|
||||
|
||||
expect(options).toEqual({
|
||||
pluginName: "@acme/plugin-linear",
|
||||
outputDir: path.resolve(cwd, "plugins", "plugin-linear"),
|
||||
template: "connector",
|
||||
category: "automation",
|
||||
displayName: "Linear Bridge",
|
||||
description: "Syncs Linear issues",
|
||||
author: "Acme",
|
||||
sdkPath: "../paperclip/packages/plugins/sdk",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds exact next commands using the scaffold path", () => {
|
||||
expect(buildPluginInitNextCommands("/tmp/acme plugin")).toEqual([
|
||||
"cd '/tmp/acme plugin'",
|
||||
"pnpm install",
|
||||
"pnpm dev",
|
||||
"paperclipai plugin install '/tmp/acme plugin'",
|
||||
]);
|
||||
});
|
||||
|
||||
it("registers the CLI wrapper and invokes the existing scaffolder", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
||||
registerPluginCommands(program);
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"plugin",
|
||||
"init",
|
||||
"demo-plugin",
|
||||
"--output",
|
||||
"/tmp/paperclip-init-output",
|
||||
"--template",
|
||||
"workspace",
|
||||
"--category",
|
||||
"workspace",
|
||||
"--display-name",
|
||||
"Demo Plugin",
|
||||
"--description",
|
||||
"Demo description",
|
||||
"--author",
|
||||
"Paperclip",
|
||||
"--sdk-path",
|
||||
"/repo/packages/plugins/sdk",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
expect(mocks.scaffoldPluginProject).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.scaffoldPluginProject).toHaveBeenCalledWith({
|
||||
pluginName: "demo-plugin",
|
||||
outputDir: path.resolve("/tmp/paperclip-init-output", "demo-plugin"),
|
||||
template: "workspace",
|
||||
category: "workspace",
|
||||
displayName: "Demo Plugin",
|
||||
description: "Demo description",
|
||||
author: "Paperclip",
|
||||
sdkPath: "/repo/packages/plugins/sdk",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { Command, Option } from "commander";
|
||||
import {
|
||||
scaffoldPluginProject,
|
||||
type ScaffoldPluginOptions,
|
||||
} from "@paperclipai/create-paperclip-plugin";
|
||||
import pc from "picocolors";
|
||||
import {
|
||||
addCommonClientOptions,
|
||||
@@ -43,6 +47,21 @@ interface PluginUninstallOptions extends BaseClientOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
interface PluginInitOptions extends BaseClientOptions {
|
||||
output?: string;
|
||||
template?: ScaffoldPluginOptions["template"];
|
||||
category?: ScaffoldPluginOptions["category"];
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
sdkPath?: string;
|
||||
}
|
||||
|
||||
interface PluginInitResult {
|
||||
outputDir: string;
|
||||
nextCommands: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -87,6 +106,63 @@ function formatPlugin(p: PluginRecord): string {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function packageToDirName(pluginName: string): string {
|
||||
return pluginName.replace(/^@[^/]+\//, "");
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
|
||||
return `'${value.replace(/'/g, "'\"'\"'")}'`;
|
||||
}
|
||||
|
||||
export function buildPluginInitScaffoldOptions(
|
||||
packageName: string,
|
||||
opts: PluginInitOptions,
|
||||
cwd = process.cwd(),
|
||||
): ScaffoldPluginOptions {
|
||||
const outputRoot = path.resolve(cwd, opts.output ?? ".");
|
||||
const outputDir = path.resolve(outputRoot, packageToDirName(packageName));
|
||||
|
||||
return {
|
||||
pluginName: packageName,
|
||||
outputDir,
|
||||
template: opts.template,
|
||||
category: opts.category,
|
||||
displayName: opts.displayName,
|
||||
description: opts.description,
|
||||
author: opts.author,
|
||||
sdkPath: opts.sdkPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPluginInitNextCommands(outputDir: string): string[] {
|
||||
const quotedOutputDir = shellQuote(outputDir);
|
||||
return [
|
||||
`cd ${quotedOutputDir}`,
|
||||
"pnpm install",
|
||||
"pnpm dev",
|
||||
`paperclipai plugin install ${quotedOutputDir}`,
|
||||
];
|
||||
}
|
||||
|
||||
export function renderPluginInitSuccess(result: PluginInitResult): string {
|
||||
return [
|
||||
pc.green(`✓ Created plugin scaffold at ${result.outputDir}`),
|
||||
"",
|
||||
"Next commands:",
|
||||
...result.nextCommands.map((command) => ` ${pc.cyan(command)}`),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function runPluginInitCommand(packageName: string, opts: PluginInitOptions): PluginInitResult {
|
||||
const scaffoldOptions = buildPluginInitScaffoldOptions(packageName, opts);
|
||||
const outputDir = scaffoldPluginProject(scaffoldOptions);
|
||||
return {
|
||||
outputDir,
|
||||
nextCommands: buildPluginInitNextCommands(outputDir),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command registration
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -94,6 +170,43 @@ function formatPlugin(p: PluginRecord): string {
|
||||
export function registerPluginCommands(program: Command): void {
|
||||
const plugin = program.command("plugin").description("Plugin lifecycle management");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// plugin init <package-name>
|
||||
// -------------------------------------------------------------------------
|
||||
addCommonClientOptions(
|
||||
plugin
|
||||
.command("init <packageName>")
|
||||
.description("Scaffold a local Paperclip plugin project")
|
||||
.option("--output <dir>", "Directory to create the plugin folder in")
|
||||
.addOption(
|
||||
new Option("--template <template>", "Starter template")
|
||||
.choices(["default", "connector", "workspace", "environment"])
|
||||
.default("default"),
|
||||
)
|
||||
.addOption(
|
||||
new Option("--category <category>", "Manifest category")
|
||||
.choices(["connector", "workspace", "automation", "ui", "environment"]),
|
||||
)
|
||||
.option("--display-name <name>", "Manifest display name")
|
||||
.option("--description <description>", "Manifest description")
|
||||
.option("--author <author>", "Manifest author")
|
||||
.option("--sdk-path <path>", "Local @paperclipai/plugin-sdk package path")
|
||||
.action((packageName: string, opts: PluginInitOptions) => {
|
||||
try {
|
||||
const result = runPluginInitCommand(packageName, opts);
|
||||
|
||||
if (opts.json) {
|
||||
printOutput(result, { json: true });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderPluginInitSuccess(result));
|
||||
} catch (err) {
|
||||
handleCommandError(err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// plugin list
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
"outDir": "dist",
|
||||
"rootDir": ".."
|
||||
},
|
||||
"include": ["src", "../packages/shared/src"]
|
||||
"include": ["src", "../packages/shared/src", "../packages/plugins/create-paperclip-plugin/src"]
|
||||
}
|
||||
Reference in new issue
Block a user