@ericsanchezok/synergy-plugin-kit 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +25 -0
- package/dist/cmd.d.ts +6 -0
- package/dist/cmd.js +3 -0
- package/dist/commands/build.d.ts +6 -0
- package/dist/commands/build.js +194 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +416 -0
- package/dist/commands/dev.d.ts +9 -0
- package/dist/commands/dev.js +192 -0
- package/dist/commands/entry.d.ts +19 -0
- package/dist/commands/entry.js +71 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/pack.d.ts +8 -0
- package/dist/commands/pack.js +64 -0
- package/dist/commands/publish-market.d.ts +23 -0
- package/dist/commands/publish-market.js +224 -0
- package/dist/commands/sign.d.ts +10 -0
- package/dist/commands/sign.js +120 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +40 -0
- package/dist/commands/validate.d.ts +10 -0
- package/dist/commands/validate.js +348 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/capability.d.ts +2 -0
- package/dist/lib/capability.js +42 -0
- package/dist/lib/crypto.d.ts +5 -0
- package/dist/lib/crypto.js +27 -0
- package/dist/lib/hash.d.ts +3 -0
- package/dist/lib/hash.js +14 -0
- package/dist/lib/ids.d.ts +3 -0
- package/dist/lib/ids.js +7 -0
- package/dist/lib/market-entry.d.ts +57 -0
- package/dist/lib/market-entry.js +181 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +5 -0
- package/dist/lib/risk.d.ts +2 -0
- package/dist/lib/risk.js +28 -0
- package/dist/lib/runtime-discovery.d.ts +12 -0
- package/dist/lib/runtime-discovery.js +13 -0
- package/dist/lib/runtime-mode.d.ts +9 -0
- package/dist/lib/runtime-mode.js +15 -0
- package/dist/lib/runtime-policy.d.ts +12 -0
- package/dist/lib/runtime-policy.js +62 -0
- package/dist/lib/signature.d.ts +15 -0
- package/dist/lib/signature.js +15 -0
- package/dist/lib/spec.d.ts +7 -0
- package/dist/lib/spec.js +21 -0
- package/dist/ui.d.ts +15 -0
- package/dist/ui.js +26 -0
- package/package.json +43 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
4
|
+
import { PluginBuildCommand, PluginCreateCommand, PluginDevCommand, PluginEntryCommand, PluginPackCommand, PluginPublishMarketCommand, PluginSignCommand, PluginTestCommand, PluginValidateCommand, } from "./commands";
|
|
5
|
+
function assertBunRuntime() {
|
|
6
|
+
if (!process.versions.bun) {
|
|
7
|
+
throw new Error("synergy-plugin requires Bun. Install Bun from https://bun.sh and retry.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
assertBunRuntime();
|
|
11
|
+
await yargs(hideBin(process.argv))
|
|
12
|
+
.scriptName("synergy-plugin")
|
|
13
|
+
.command(PluginCreateCommand)
|
|
14
|
+
.command(PluginValidateCommand)
|
|
15
|
+
.command(PluginDevCommand)
|
|
16
|
+
.command(PluginBuildCommand)
|
|
17
|
+
.command(PluginPackCommand)
|
|
18
|
+
.command(PluginSignCommand)
|
|
19
|
+
.command(PluginTestCommand)
|
|
20
|
+
.command(PluginPublishMarketCommand)
|
|
21
|
+
.command(PluginEntryCommand)
|
|
22
|
+
.demandCommand(1)
|
|
23
|
+
.strict()
|
|
24
|
+
.help()
|
|
25
|
+
.parse();
|
package/dist/cmd.d.ts
ADDED
package/dist/cmd.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { EOL } from "os";
|
|
4
|
+
import { PluginManifest } from "@ericsanchezok/synergy-plugin";
|
|
5
|
+
import { cmd } from "../cmd";
|
|
6
|
+
import { UI } from "../ui";
|
|
7
|
+
import { sha256File, sha256JSON } from "../lib/crypto";
|
|
8
|
+
function ensureDir(dirPath) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
function copyDir(src, dest) {
|
|
12
|
+
ensureDir(dest);
|
|
13
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
14
|
+
const srcPath = path.join(src, entry.name);
|
|
15
|
+
const destPath = path.join(dest, entry.name);
|
|
16
|
+
if (entry.isDirectory())
|
|
17
|
+
copyDir(srcPath, destPath);
|
|
18
|
+
else
|
|
19
|
+
fs.copyFileSync(srcPath, destPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function copyFilePreserve(pluginDir, distDir, relativePath) {
|
|
23
|
+
const normalized = relativePath.replace(/^\.\//, "");
|
|
24
|
+
const src = path.resolve(pluginDir, normalized);
|
|
25
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile())
|
|
26
|
+
return;
|
|
27
|
+
const dest = path.join(distDir, normalized);
|
|
28
|
+
ensureDir(path.dirname(dest));
|
|
29
|
+
fs.copyFileSync(src, dest);
|
|
30
|
+
}
|
|
31
|
+
function findUiSource(pluginDir) {
|
|
32
|
+
const candidates = ["src/ui.tsx", "src/ui/index.tsx", "src/ui.ts", "src/ui/index.ts"];
|
|
33
|
+
return candidates.map((candidate) => path.join(pluginDir, candidate)).find((candidate) => fs.existsSync(candidate));
|
|
34
|
+
}
|
|
35
|
+
function packagedManifest(manifest) {
|
|
36
|
+
const next = structuredClone(manifest);
|
|
37
|
+
next.main = "./runtime/index.js";
|
|
38
|
+
if (next.contributes?.ui?.entry) {
|
|
39
|
+
next.contributes.ui.entry = next.contributes.ui.entry.replace(/^\.\//, "").replace(/^dist\//, "./");
|
|
40
|
+
if (!next.contributes.ui.entry.startsWith("."))
|
|
41
|
+
next.contributes.ui.entry = `./${next.contributes.ui.entry}`;
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
function permissionSummary(manifest) {
|
|
46
|
+
const perms = manifest.permissions ?? {};
|
|
47
|
+
const result = {};
|
|
48
|
+
if (perms.tools)
|
|
49
|
+
result.tools = perms.tools;
|
|
50
|
+
if (perms.data)
|
|
51
|
+
result.data = perms.data;
|
|
52
|
+
if (perms.network)
|
|
53
|
+
result.network = perms.network;
|
|
54
|
+
if (perms.ui)
|
|
55
|
+
result.ui = perms.ui;
|
|
56
|
+
if (perms.hooks)
|
|
57
|
+
result.hooks = perms.hooks;
|
|
58
|
+
const tools = manifest.contributes?.tools ?? [];
|
|
59
|
+
if (tools.length > 0) {
|
|
60
|
+
const toolPerms = {};
|
|
61
|
+
for (const tool of tools) {
|
|
62
|
+
if (tool.capabilities)
|
|
63
|
+
toolPerms[tool.name] = tool.capabilities;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(toolPerms).length > 0)
|
|
66
|
+
result.contributedTools = toolPerms;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
export async function buildPluginProject(pluginDir) {
|
|
71
|
+
const manifestPath = path.join(pluginDir, "plugin.json");
|
|
72
|
+
if (!fs.existsSync(manifestPath)) {
|
|
73
|
+
UI.error(`No plugin.json found at ${manifestPath}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
let manifest;
|
|
77
|
+
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
78
|
+
const parsed = PluginManifest.safeParse(raw);
|
|
79
|
+
if (!parsed.success) {
|
|
80
|
+
UI.error("Invalid plugin manifest:");
|
|
81
|
+
for (const issue of parsed.error.issues) {
|
|
82
|
+
UI.println(` ${UI.Style.TEXT_DIM}${issue.path.join(".")}:${UI.Style.TEXT_NORMAL} ${issue.message}`);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
manifest = parsed.data;
|
|
87
|
+
const spinner = (message) => {
|
|
88
|
+
process.stderr.write(`${UI.Style.TEXT_DIM} ${message}...${UI.Style.TEXT_NORMAL}${EOL}`);
|
|
89
|
+
};
|
|
90
|
+
UI.println(`${UI.Style.TEXT_NORMAL_BOLD}Building${UI.Style.TEXT_NORMAL} ${manifest.name} v${manifest.version}`);
|
|
91
|
+
UI.println(` ${UI.Style.TEXT_DIM}Source:${UI.Style.TEXT_NORMAL} ${pluginDir}`);
|
|
92
|
+
const distDir = path.join(pluginDir, "dist");
|
|
93
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
94
|
+
ensureDir(distDir);
|
|
95
|
+
const entryFile = manifest.main ?? "./src/index.ts";
|
|
96
|
+
const entryPath = path.resolve(pluginDir, entryFile);
|
|
97
|
+
const runtimeOutdir = path.join(distDir, "runtime");
|
|
98
|
+
spinner("Building backend");
|
|
99
|
+
const backendResult = await Bun.build({
|
|
100
|
+
entrypoints: [entryPath],
|
|
101
|
+
outdir: runtimeOutdir,
|
|
102
|
+
target: "bun",
|
|
103
|
+
naming: "index.js",
|
|
104
|
+
external: ["@ericsanchezok/synergy-plugin", "@ericsanchezok/synergy-sdk", "@ericsanchezok/synergy-util"],
|
|
105
|
+
});
|
|
106
|
+
if (!backendResult.success) {
|
|
107
|
+
for (const log of backendResult.logs) {
|
|
108
|
+
UI.println(` ${UI.Style.TEXT_WARNING}${log.message}${UI.Style.TEXT_NORMAL}`);
|
|
109
|
+
}
|
|
110
|
+
UI.error("Backend build failed");
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const uiEntry = manifest.contributes?.ui?.entry;
|
|
114
|
+
if (uiEntry) {
|
|
115
|
+
const uiSourcePath = findUiSource(pluginDir);
|
|
116
|
+
const uiOutputPath = path.resolve(pluginDir, uiEntry);
|
|
117
|
+
if (uiSourcePath) {
|
|
118
|
+
const uiOutdir = path.dirname(uiOutputPath);
|
|
119
|
+
spinner("Building frontend");
|
|
120
|
+
const frontendResult = await Bun.build({
|
|
121
|
+
entrypoints: [uiSourcePath],
|
|
122
|
+
outdir: uiOutdir,
|
|
123
|
+
target: "browser",
|
|
124
|
+
naming: path.basename(uiOutputPath),
|
|
125
|
+
});
|
|
126
|
+
if (!frontendResult.success) {
|
|
127
|
+
for (const log of frontendResult.logs) {
|
|
128
|
+
UI.println(` ${UI.Style.TEXT_WARNING}${log.message}${UI.Style.TEXT_NORMAL}`);
|
|
129
|
+
}
|
|
130
|
+
UI.error("Frontend build failed");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (!fs.existsSync(uiOutputPath)) {
|
|
135
|
+
UI.error(`UI source not found. Expected one of src/ui.tsx or src/ui/index.tsx for ${uiEntry}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
spinner("Normalizing manifest");
|
|
140
|
+
const distManifest = packagedManifest(manifest);
|
|
141
|
+
const distManifestPath = path.join(distDir, "plugin.json");
|
|
142
|
+
fs.writeFileSync(distManifestPath, JSON.stringify(distManifest, null, 2));
|
|
143
|
+
const normalizedPath = path.join(distDir, "plugin.normalized.json");
|
|
144
|
+
fs.writeFileSync(normalizedPath, JSON.stringify(distManifest, null, 2));
|
|
145
|
+
const packageJsonPath = path.join(pluginDir, "package.json");
|
|
146
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
147
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
148
|
+
pkg.main = "./runtime/index.js";
|
|
149
|
+
pkg.exports = { ".": "./runtime/index.js" };
|
|
150
|
+
fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
151
|
+
}
|
|
152
|
+
spinner("Generating permission summary");
|
|
153
|
+
const summary = permissionSummary(manifest);
|
|
154
|
+
fs.writeFileSync(path.join(distDir, "permissions.summary.json"), JSON.stringify(summary, null, 2));
|
|
155
|
+
const publicAssetsPath = path.join(pluginDir, "public", "assets");
|
|
156
|
+
if (fs.existsSync(publicAssetsPath)) {
|
|
157
|
+
spinner("Copying assets");
|
|
158
|
+
copyDir(publicAssetsPath, path.join(distDir, "assets"));
|
|
159
|
+
}
|
|
160
|
+
for (const theme of manifest.contributes?.ui?.themes ?? [])
|
|
161
|
+
copyFilePreserve(pluginDir, distDir, theme.path);
|
|
162
|
+
for (const icon of manifest.contributes?.ui?.icons ?? [])
|
|
163
|
+
copyFilePreserve(pluginDir, distDir, icon.path);
|
|
164
|
+
spinner("Computing integrity hashes");
|
|
165
|
+
const integrity = {
|
|
166
|
+
manifest: sha256File(distManifestPath),
|
|
167
|
+
permissions: sha256JSON(summary),
|
|
168
|
+
};
|
|
169
|
+
const runtimeIndex = path.join(runtimeOutdir, "index.js");
|
|
170
|
+
if (fs.existsSync(runtimeIndex))
|
|
171
|
+
integrity.runtime = sha256File(runtimeIndex);
|
|
172
|
+
if (uiEntry) {
|
|
173
|
+
const uiIndex = path.resolve(pluginDir, uiEntry);
|
|
174
|
+
if (fs.existsSync(uiIndex))
|
|
175
|
+
integrity.ui = sha256File(uiIndex);
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(path.join(distDir, "integrity.json"), JSON.stringify(integrity, null, 2));
|
|
178
|
+
UI.println(`${UI.Style.TEXT_SUCCESS}✔${UI.Style.TEXT_NORMAL} Built ${manifest.name} v${manifest.version} -> ${distDir}`);
|
|
179
|
+
UI.println(` ${UI.Style.TEXT_DIM}Output:${UI.Style.TEXT_NORMAL} ${distDir}`);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
export const PluginBuildCommand = cmd({
|
|
183
|
+
command: "build [path]",
|
|
184
|
+
describe: "build a plugin for distribution",
|
|
185
|
+
builder: (yargs) => yargs.positional("path", {
|
|
186
|
+
type: "string",
|
|
187
|
+
describe: "path to plugin directory (defaults to cwd)",
|
|
188
|
+
}),
|
|
189
|
+
async handler(args) {
|
|
190
|
+
const ok = await buildPluginProject(path.resolve(args.path ?? process.cwd()));
|
|
191
|
+
if (!ok)
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { EOL } from "os";
|
|
4
|
+
import { cmd } from "../cmd";
|
|
5
|
+
import { UI } from "../ui";
|
|
6
|
+
const TEMPLATES = ["tool-ui", "workspace-panel", "api-connector", "theme-icon"];
|
|
7
|
+
function pluginJson(name, extra) {
|
|
8
|
+
const base = {
|
|
9
|
+
name,
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
description: `${name} plugin`,
|
|
12
|
+
permissions: {},
|
|
13
|
+
contributes: {},
|
|
14
|
+
};
|
|
15
|
+
return JSON.stringify({ ...base, ...extra(name) }, null, 2) + EOL;
|
|
16
|
+
}
|
|
17
|
+
function packageJson(name, templateName) {
|
|
18
|
+
const needsSolid = templateName !== "theme-icon";
|
|
19
|
+
const pkg = {
|
|
20
|
+
name,
|
|
21
|
+
version: "0.1.0",
|
|
22
|
+
type: "module",
|
|
23
|
+
scripts: {
|
|
24
|
+
dev: "synergy-plugin dev",
|
|
25
|
+
validate: "synergy-plugin validate --runtime-discovery",
|
|
26
|
+
build: "synergy-plugin build",
|
|
27
|
+
pack: "synergy-plugin pack",
|
|
28
|
+
sign: "synergy-plugin sign",
|
|
29
|
+
"publish:market": "synergy-plugin publish-market",
|
|
30
|
+
test: "synergy-plugin test",
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
"@ericsanchezok/synergy-plugin": "latest",
|
|
34
|
+
zod: "^4.0.0",
|
|
35
|
+
...(needsSolid ? { "solid-js": "^1.9.0" } : {}),
|
|
36
|
+
},
|
|
37
|
+
devDependencies: {
|
|
38
|
+
"@ericsanchezok/synergy-plugin-kit": "latest",
|
|
39
|
+
typescript: "^5.8.0",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return JSON.stringify(pkg, null, 2) + EOL;
|
|
43
|
+
}
|
|
44
|
+
function tsconfigJson() {
|
|
45
|
+
return (JSON.stringify({
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
target: "ESNext",
|
|
48
|
+
module: "ESNext",
|
|
49
|
+
moduleResolution: "bundler",
|
|
50
|
+
strict: true,
|
|
51
|
+
esModuleInterop: true,
|
|
52
|
+
skipLibCheck: true,
|
|
53
|
+
outDir: "dist",
|
|
54
|
+
rootDir: "src",
|
|
55
|
+
jsx: "preserve",
|
|
56
|
+
jsxImportSource: "solid-js",
|
|
57
|
+
},
|
|
58
|
+
include: ["src"],
|
|
59
|
+
}, null, 2) + EOL);
|
|
60
|
+
}
|
|
61
|
+
function readmeMd(name) {
|
|
62
|
+
return `# ${name}
|
|
63
|
+
|
|
64
|
+
Synergy plugin generated with \`synergy-plugin create\`.
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
\`\`\`bash
|
|
69
|
+
bun install
|
|
70
|
+
bun run validate
|
|
71
|
+
bun run build
|
|
72
|
+
bun run pack
|
|
73
|
+
bun run sign ${name}-0.1.0.synergy-plugin.tgz
|
|
74
|
+
bun run publish:market
|
|
75
|
+
\`\`\`
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function indexToolUI(name) {
|
|
79
|
+
return `import type { PluginDescriptor, PluginInput, PluginHooks } from "@ericsanchezok/synergy-plugin"
|
|
80
|
+
import { greet } from "./tools"
|
|
81
|
+
|
|
82
|
+
export const plugin: PluginDescriptor = {
|
|
83
|
+
id: "${name}",
|
|
84
|
+
name: "${name}",
|
|
85
|
+
async init(_input: PluginInput): Promise<PluginHooks> {
|
|
86
|
+
return {
|
|
87
|
+
tool: {
|
|
88
|
+
greet,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default plugin
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
function indexWorkspacePanel(name) {
|
|
98
|
+
return `import type { PluginDescriptor, PluginInput, PluginHooks } from "@ericsanchezok/synergy-plugin"
|
|
99
|
+
|
|
100
|
+
export const plugin: PluginDescriptor = {
|
|
101
|
+
id: "${name}",
|
|
102
|
+
name: "${name}",
|
|
103
|
+
async init(_input: PluginInput): Promise<PluginHooks> {
|
|
104
|
+
return {}
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default plugin
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function indexApiConnector(name) {
|
|
112
|
+
return `import type { PluginDescriptor, PluginInput, PluginHooks } from "@ericsanchezok/synergy-plugin"
|
|
113
|
+
import { getJSON, postJSON } from "./tools"
|
|
114
|
+
|
|
115
|
+
export const plugin: PluginDescriptor = {
|
|
116
|
+
id: "${name}",
|
|
117
|
+
name: "${name}",
|
|
118
|
+
async init(_input: PluginInput): Promise<PluginHooks> {
|
|
119
|
+
return {
|
|
120
|
+
tool: {
|
|
121
|
+
getJSON,
|
|
122
|
+
postJSON,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default plugin
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
function indexThemeIcon(name) {
|
|
132
|
+
return `import type { PluginDescriptor, PluginInput, PluginHooks } from "@ericsanchezok/synergy-plugin"
|
|
133
|
+
|
|
134
|
+
export const plugin: PluginDescriptor = {
|
|
135
|
+
id: "${name}",
|
|
136
|
+
name: "${name}",
|
|
137
|
+
async init(_input: PluginInput): Promise<PluginHooks> {
|
|
138
|
+
return {}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default plugin
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
function toolsToolUI(_name) {
|
|
146
|
+
return `import { tool } from "@ericsanchezok/synergy-plugin/tool"
|
|
147
|
+
|
|
148
|
+
export const greet = tool({
|
|
149
|
+
description: "Greet a user by name",
|
|
150
|
+
args: {
|
|
151
|
+
name: tool.schema.string().describe("The name to greet"),
|
|
152
|
+
},
|
|
153
|
+
async execute(args) {
|
|
154
|
+
return { output: \`Hello, \${args.name}!\` }
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
function toolsApiConnector(_name) {
|
|
160
|
+
return `import { tool } from "@ericsanchezok/synergy-plugin/tool"
|
|
161
|
+
|
|
162
|
+
export const getJSON = tool({
|
|
163
|
+
description: "Fetch and parse JSON from an API endpoint",
|
|
164
|
+
args: {
|
|
165
|
+
url: tool.schema.string().describe("The API endpoint URL"),
|
|
166
|
+
},
|
|
167
|
+
async execute(args) {
|
|
168
|
+
const res = await fetch(args.url)
|
|
169
|
+
const json = await res.json()
|
|
170
|
+
return { output: JSON.stringify(json, null, 2) }
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export const postJSON = tool({
|
|
175
|
+
description: "POST JSON to an API endpoint",
|
|
176
|
+
args: {
|
|
177
|
+
url: tool.schema.string().describe("The API endpoint URL"),
|
|
178
|
+
body: tool.schema.string().describe("The JSON request body"),
|
|
179
|
+
},
|
|
180
|
+
async execute(args) {
|
|
181
|
+
const res = await fetch(args.url, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: args.body,
|
|
185
|
+
})
|
|
186
|
+
const text = await res.text()
|
|
187
|
+
return { output: text }
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
function uiToolUI(_name) {
|
|
193
|
+
return `import type { Component } from "solid-js"
|
|
194
|
+
|
|
195
|
+
interface Props {
|
|
196
|
+
tool: string
|
|
197
|
+
output: string
|
|
198
|
+
metadata?: Record<string, unknown>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const ToolRenderer: Component<Props> = (props) => {
|
|
202
|
+
return <div>{props.output}</div>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default ToolRenderer
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
function uiWorkspacePanel(_name) {
|
|
209
|
+
return `import type { Component } from "solid-js"
|
|
210
|
+
|
|
211
|
+
const WorkspacePanel: Component = () => {
|
|
212
|
+
return <div>Workspace panel content</div>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default WorkspacePanel
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
function manifestToolUI(name) {
|
|
219
|
+
return {
|
|
220
|
+
permissions: {
|
|
221
|
+
tools: {
|
|
222
|
+
invoke: true,
|
|
223
|
+
shell: false,
|
|
224
|
+
filesystem: "none",
|
|
225
|
+
network: false,
|
|
226
|
+
mcp: "none",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
contributes: {
|
|
230
|
+
tools: [
|
|
231
|
+
{
|
|
232
|
+
name: "greet",
|
|
233
|
+
title: "Greet",
|
|
234
|
+
description: "Greet a user by name",
|
|
235
|
+
capabilities: {
|
|
236
|
+
filesystem: "none",
|
|
237
|
+
network: false,
|
|
238
|
+
shell: false,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
ui: {
|
|
243
|
+
entry: "./dist/ui/index.js",
|
|
244
|
+
toolRenderers: [{ tool: "greet" }],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function manifestWorkspacePanel(name) {
|
|
250
|
+
return {
|
|
251
|
+
contributes: {
|
|
252
|
+
ui: {
|
|
253
|
+
entry: "./dist/ui/index.js",
|
|
254
|
+
workspacePanels: [
|
|
255
|
+
{
|
|
256
|
+
id: `${name}-panel`,
|
|
257
|
+
label: name,
|
|
258
|
+
icon: "layout-panel-left",
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function manifestApiConnector(_name) {
|
|
266
|
+
return {
|
|
267
|
+
permissions: {
|
|
268
|
+
tools: {
|
|
269
|
+
invoke: true,
|
|
270
|
+
network: true,
|
|
271
|
+
shell: false,
|
|
272
|
+
filesystem: "none",
|
|
273
|
+
mcp: "none",
|
|
274
|
+
},
|
|
275
|
+
network: {
|
|
276
|
+
connectDomains: ["*"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
contributes: {
|
|
280
|
+
tools: [
|
|
281
|
+
{
|
|
282
|
+
name: "getJSON",
|
|
283
|
+
title: "Get JSON",
|
|
284
|
+
description: "Fetch and parse JSON from an API endpoint",
|
|
285
|
+
capabilities: {
|
|
286
|
+
network: true,
|
|
287
|
+
filesystem: "none",
|
|
288
|
+
shell: false,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "postJSON",
|
|
293
|
+
title: "Post JSON",
|
|
294
|
+
description: "POST JSON to an API endpoint",
|
|
295
|
+
capabilities: {
|
|
296
|
+
network: true,
|
|
297
|
+
filesystem: "none",
|
|
298
|
+
shell: false,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
ui: {
|
|
303
|
+
entry: "./dist/ui/index.js",
|
|
304
|
+
toolRenderers: [{ tool: "getJSON" }, { tool: "postJSON" }],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function manifestThemeIcon(name) {
|
|
310
|
+
return {
|
|
311
|
+
contributes: {
|
|
312
|
+
ui: {
|
|
313
|
+
themes: [{ id: `${name}-theme`, label: name, path: "./themes/default.css" }],
|
|
314
|
+
icons: [{ name: `${name}-logo`, path: "./icons/logo.svg" }],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const TEMPLATE_DEFS = {
|
|
320
|
+
"tool-ui": {
|
|
321
|
+
label: "Tool UI - tool definitions with Solid tool renderer",
|
|
322
|
+
manifest: manifestToolUI,
|
|
323
|
+
files: [
|
|
324
|
+
{ relativePath: "src/index.ts", content: indexToolUI },
|
|
325
|
+
{ relativePath: "src/tools.ts", content: toolsToolUI },
|
|
326
|
+
{ relativePath: "src/ui.tsx", content: uiToolUI },
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
"workspace-panel": {
|
|
330
|
+
label: "Workspace Panel - Solid workspace panel with no tools",
|
|
331
|
+
manifest: manifestWorkspacePanel,
|
|
332
|
+
files: [
|
|
333
|
+
{ relativePath: "src/index.ts", content: indexWorkspacePanel },
|
|
334
|
+
{ relativePath: "src/ui.tsx", content: uiWorkspacePanel },
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
"api-connector": {
|
|
338
|
+
label: "API Connector - network-enabled tools for API integration",
|
|
339
|
+
manifest: manifestApiConnector,
|
|
340
|
+
files: [
|
|
341
|
+
{ relativePath: "src/index.ts", content: indexApiConnector },
|
|
342
|
+
{ relativePath: "src/tools.ts", content: toolsApiConnector },
|
|
343
|
+
{ relativePath: "src/ui.tsx", content: uiToolUI },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
"theme-icon": {
|
|
347
|
+
label: "Theme & Icon - themes and icon contributions",
|
|
348
|
+
manifest: manifestThemeIcon,
|
|
349
|
+
files: [
|
|
350
|
+
{ relativePath: "src/index.ts", content: indexThemeIcon },
|
|
351
|
+
{ relativePath: "themes/default.css", content: () => ":root { --plugin-accent: #2563eb; }\n" },
|
|
352
|
+
{
|
|
353
|
+
relativePath: "icons/logo.svg",
|
|
354
|
+
content: () => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>\n',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
function scaffold(name, templateName, template, targetDir) {
|
|
360
|
+
const created = [];
|
|
361
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
362
|
+
const files = [
|
|
363
|
+
{ relativePath: "plugin.json", content: (pluginName) => pluginJson(pluginName, template.manifest) },
|
|
364
|
+
{ relativePath: "package.json", content: (pluginName) => packageJson(pluginName, templateName) },
|
|
365
|
+
{ relativePath: "tsconfig.json", content: tsconfigJson },
|
|
366
|
+
{ relativePath: "README.md", content: readmeMd },
|
|
367
|
+
...template.files,
|
|
368
|
+
];
|
|
369
|
+
for (const file of files) {
|
|
370
|
+
const filePath = path.join(targetDir, file.relativePath);
|
|
371
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
372
|
+
fs.writeFileSync(filePath, file.content(name));
|
|
373
|
+
created.push(filePath);
|
|
374
|
+
}
|
|
375
|
+
return created;
|
|
376
|
+
}
|
|
377
|
+
export const PluginCreateCommand = cmd({
|
|
378
|
+
command: "create <name>",
|
|
379
|
+
describe: "scaffold a new Synergy plugin project",
|
|
380
|
+
builder: (yargs) => yargs
|
|
381
|
+
.positional("name", {
|
|
382
|
+
type: "string",
|
|
383
|
+
describe: "plugin name (used as directory name and plugin id)",
|
|
384
|
+
demandOption: true,
|
|
385
|
+
})
|
|
386
|
+
.option("template", {
|
|
387
|
+
type: "string",
|
|
388
|
+
describe: "template to scaffold",
|
|
389
|
+
choices: [...TEMPLATES],
|
|
390
|
+
default: "tool-ui",
|
|
391
|
+
}),
|
|
392
|
+
async handler(args) {
|
|
393
|
+
const name = args.name;
|
|
394
|
+
const templateName = args.template ?? "tool-ui";
|
|
395
|
+
const template = TEMPLATE_DEFS[templateName];
|
|
396
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
397
|
+
if (!/^[a-z0-9][-a-z0-9]*$/.test(name)) {
|
|
398
|
+
UI.error(`Invalid plugin name "${name}". Use lowercase letters, digits, and hyphens.`);
|
|
399
|
+
process.exitCode = 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (fs.existsSync(targetDir)) {
|
|
403
|
+
UI.error(`Directory "${targetDir}" already exists. Remove it first or use a different name.`);
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const created = scaffold(name, templateName, template, targetDir);
|
|
408
|
+
UI.println(`${UI.Style.TEXT_SUCCESS}✔${UI.Style.TEXT_NORMAL} Created plugin "${name}" (${template.label})`);
|
|
409
|
+
UI.println();
|
|
410
|
+
for (const filePath of created) {
|
|
411
|
+
UI.println(` ${UI.Style.TEXT_DIM}${path.relative(process.cwd(), filePath)}${UI.Style.TEXT_NORMAL}`);
|
|
412
|
+
}
|
|
413
|
+
UI.println();
|
|
414
|
+
UI.println(`${UI.Style.TEXT_DIM}Next: cd ${name} && bun install && bun run validate${UI.Style.TEXT_NORMAL}`);
|
|
415
|
+
},
|
|
416
|
+
});
|