@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.
Files changed (53) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +25 -0
  3. package/dist/cmd.d.ts +6 -0
  4. package/dist/cmd.js +3 -0
  5. package/dist/commands/build.d.ts +6 -0
  6. package/dist/commands/build.js +194 -0
  7. package/dist/commands/create.d.ts +9 -0
  8. package/dist/commands/create.js +416 -0
  9. package/dist/commands/dev.d.ts +9 -0
  10. package/dist/commands/dev.js +192 -0
  11. package/dist/commands/entry.d.ts +19 -0
  12. package/dist/commands/entry.js +71 -0
  13. package/dist/commands/index.d.ts +9 -0
  14. package/dist/commands/index.js +9 -0
  15. package/dist/commands/pack.d.ts +8 -0
  16. package/dist/commands/pack.js +64 -0
  17. package/dist/commands/publish-market.d.ts +23 -0
  18. package/dist/commands/publish-market.js +224 -0
  19. package/dist/commands/sign.d.ts +10 -0
  20. package/dist/commands/sign.js +120 -0
  21. package/dist/commands/test.d.ts +5 -0
  22. package/dist/commands/test.js +40 -0
  23. package/dist/commands/validate.d.ts +10 -0
  24. package/dist/commands/validate.js +348 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +3 -0
  27. package/dist/lib/capability.d.ts +2 -0
  28. package/dist/lib/capability.js +42 -0
  29. package/dist/lib/crypto.d.ts +5 -0
  30. package/dist/lib/crypto.js +27 -0
  31. package/dist/lib/hash.d.ts +3 -0
  32. package/dist/lib/hash.js +14 -0
  33. package/dist/lib/ids.d.ts +3 -0
  34. package/dist/lib/ids.js +7 -0
  35. package/dist/lib/market-entry.d.ts +57 -0
  36. package/dist/lib/market-entry.js +181 -0
  37. package/dist/lib/paths.d.ts +3 -0
  38. package/dist/lib/paths.js +5 -0
  39. package/dist/lib/risk.d.ts +2 -0
  40. package/dist/lib/risk.js +28 -0
  41. package/dist/lib/runtime-discovery.d.ts +12 -0
  42. package/dist/lib/runtime-discovery.js +13 -0
  43. package/dist/lib/runtime-mode.d.ts +9 -0
  44. package/dist/lib/runtime-mode.js +15 -0
  45. package/dist/lib/runtime-policy.d.ts +12 -0
  46. package/dist/lib/runtime-policy.js +62 -0
  47. package/dist/lib/signature.d.ts +15 -0
  48. package/dist/lib/signature.js +15 -0
  49. package/dist/lib/spec.d.ts +7 -0
  50. package/dist/lib/spec.js +21 -0
  51. package/dist/ui.d.ts +15 -0
  52. package/dist/ui.js +26 -0
  53. package/package.json +43 -0
@@ -0,0 +1,192 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { PluginManifest } from "@ericsanchezok/synergy-plugin";
4
+ import { cmd } from "../cmd";
5
+ import { UI } from "../ui";
6
+ import { computeRisk } from "../lib/risk";
7
+ function timestamp() {
8
+ return new Date().toLocaleTimeString("en-US", { hour12: false });
9
+ }
10
+ function debounce(ms) {
11
+ let timer;
12
+ return (fn) => {
13
+ if (timer)
14
+ clearTimeout(timer);
15
+ timer = setTimeout(fn, ms);
16
+ };
17
+ }
18
+ function overallRisk(manifest) {
19
+ const caps = [];
20
+ const pt = manifest.permissions?.tools;
21
+ if (pt?.shell)
22
+ caps.push("shell");
23
+ if (pt?.filesystem === "write")
24
+ caps.push("filesystem:write");
25
+ else if (pt?.filesystem === "read")
26
+ caps.push("filesystem:read");
27
+ if (pt?.network)
28
+ caps.push("network");
29
+ if (pt?.mcp === "invoke")
30
+ caps.push("mcp:invoke");
31
+ if (pt?.mcp === "spawn")
32
+ caps.push("mcp:spawn");
33
+ const pd = manifest.permissions?.data;
34
+ if (pd?.session === "read")
35
+ caps.push("session_data");
36
+ if (pd?.workspace === "read")
37
+ caps.push("workspace_data");
38
+ if (pd?.secrets === "own")
39
+ caps.push("secrets");
40
+ if (pd?.config === "global")
41
+ caps.push("config:write");
42
+ if (pd?.config === "plugin")
43
+ caps.push("config:read");
44
+ return computeRisk(caps, manifest);
45
+ }
46
+ function riskLabel(risk) {
47
+ if (risk === "high")
48
+ return UI.Style.TEXT_DANGER + "high" + UI.Style.TEXT_NORMAL;
49
+ if (risk === "medium")
50
+ return UI.Style.TEXT_WARNING + "medium" + UI.Style.TEXT_NORMAL;
51
+ return UI.Style.TEXT_SUCCESS + "low" + UI.Style.TEXT_NORMAL;
52
+ }
53
+ function printPermissionPreview(manifest) {
54
+ const pt = manifest.permissions?.tools;
55
+ const pd = manifest.permissions?.data;
56
+ const risk = overallRisk(manifest);
57
+ const caps = [];
58
+ if (pt?.shell)
59
+ caps.push(`shell (${UI.Style.TEXT_DANGER}high${UI.Style.TEXT_NORMAL})`);
60
+ if (pt?.filesystem === "write")
61
+ caps.push(`filesystem: write (${UI.Style.TEXT_DANGER}high${UI.Style.TEXT_NORMAL})`);
62
+ else if (pt?.filesystem === "read")
63
+ caps.push(`filesystem: read (${UI.Style.TEXT_WARNING}medium${UI.Style.TEXT_NORMAL})`);
64
+ UI.println(`✓ permissions: ${riskLabel(risk)} risk`);
65
+ if (caps.length > 0)
66
+ UI.println(` -> tools: ${caps.join(", ")}`);
67
+ if (pt?.network) {
68
+ const net = pt.network === true
69
+ ? "all hosts"
70
+ : typeof pt.network === "object" && "hosts" in pt.network
71
+ ? pt.network.hosts.join(", ")
72
+ : String(pt.network);
73
+ UI.println(` -> network: ${net}`);
74
+ }
75
+ if (pd?.session === "read")
76
+ UI.println(" -> data: session read");
77
+ if (pd?.workspace === "read")
78
+ UI.println(" -> data: workspace read");
79
+ if (pd?.secrets === "own")
80
+ UI.println(" -> data: secrets");
81
+ }
82
+ function countUiContributions(manifest) {
83
+ const ui = manifest.contributes?.ui;
84
+ if (!ui)
85
+ return 0;
86
+ return ((ui.toolRenderers?.length ?? 0) +
87
+ (ui.partRenderers?.length ?? 0) +
88
+ (ui.workspacePanels?.length ?? 0) +
89
+ (ui.globalPanels?.length ?? 0) +
90
+ (ui.settings?.length ?? 0) +
91
+ (ui.chatComponents?.length ?? 0) +
92
+ (ui.themes?.length ?? 0) +
93
+ (ui.icons?.length ?? 0) +
94
+ (ui.commands?.length ?? 0) +
95
+ (ui.routes?.length ?? 0));
96
+ }
97
+ function readManifest(manifestPath) {
98
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
99
+ return PluginManifest.parse(raw);
100
+ }
101
+ export const PluginDevCommand = cmd({
102
+ command: "dev [path]",
103
+ describe: "start plugin development mode with file watching",
104
+ builder: (yargs) => yargs
105
+ .positional("path", {
106
+ type: "string",
107
+ describe: "path to plugin directory (defaults to cwd)",
108
+ })
109
+ .option("sandbox-preview", {
110
+ type: "boolean",
111
+ default: false,
112
+ describe: "print Synergy sandbox preview URLs for UI panels",
113
+ })
114
+ .option("port", {
115
+ type: "number",
116
+ default: 3000,
117
+ describe: "Synergy server port for preview URLs",
118
+ }),
119
+ async handler(args) {
120
+ const pluginDir = path.resolve(args.path ?? process.cwd());
121
+ const manifestPath = path.join(pluginDir, "plugin.json");
122
+ if (!fs.existsSync(manifestPath)) {
123
+ UI.error(`No plugin.json found at ${manifestPath}`);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ let manifest;
128
+ try {
129
+ manifest = readManifest(manifestPath);
130
+ }
131
+ catch (error) {
132
+ UI.error(`Failed to read manifest: ${error instanceof Error ? error.message : String(error)}`);
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ UI.println(`${UI.Style.TEXT_NORMAL_BOLD}Synergy Plugin Dev${UI.Style.TEXT_NORMAL} - ${manifest.name} v${manifest.version}`);
137
+ UI.println();
138
+ UI.println(`${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} manifest valid`);
139
+ printPermissionPreview(manifest);
140
+ const uiContribs = countUiContributions(manifest);
141
+ if (uiContribs > 0) {
142
+ UI.println();
143
+ UI.println(`UI: ${uiContribs} contribution${uiContribs !== 1 ? "s" : ""}`);
144
+ }
145
+ if (args["sandbox-preview"]) {
146
+ for (const panel of manifest.contributes?.ui?.workspacePanels ?? []) {
147
+ if (panel.sandbox) {
148
+ UI.println(` ${panel.label}: http://localhost:${args.port}/plugin/${encodeURIComponent(manifest.name)}/sandbox/${encodeURIComponent(panel.id)}`);
149
+ }
150
+ }
151
+ for (const panel of manifest.contributes?.ui?.globalPanels ?? []) {
152
+ if (panel.sandbox) {
153
+ UI.println(` ${panel.label}: http://localhost:${args.port}/plugin/${encodeURIComponent(manifest.name)}/sandbox/${encodeURIComponent(panel.id)}`);
154
+ }
155
+ }
156
+ }
157
+ const srcDir = path.join(pluginDir, "src");
158
+ if (!fs.existsSync(srcDir)) {
159
+ UI.println(`${UI.Style.TEXT_WARNING}⚠${UI.Style.TEXT_NORMAL} No src/ directory found at ${srcDir}`);
160
+ return;
161
+ }
162
+ UI.println();
163
+ UI.println(`Watching ${srcDir} for changes...`);
164
+ UI.println(`${UI.Style.TEXT_DIM}Run synergy plugin add file://${pluginDir} in a Synergy runtime when you need live installation/reload.${UI.Style.TEXT_NORMAL}`);
165
+ UI.println();
166
+ const onReload = debounce(300);
167
+ const watcher = fs.watch(srcDir, { recursive: true }, (_event, filename) => {
168
+ if (!filename)
169
+ return;
170
+ onReload(() => {
171
+ UI.println(`${timestamp()} [${manifest.name}] File changed: ${filename}`);
172
+ try {
173
+ manifest = readManifest(manifestPath);
174
+ UI.println(`${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} manifest valid`);
175
+ printPermissionPreview(manifest);
176
+ UI.println();
177
+ }
178
+ catch (error) {
179
+ UI.error(`Manifest read error: ${error instanceof Error ? error.message : String(error)}`);
180
+ }
181
+ });
182
+ });
183
+ const shutdown = () => {
184
+ UI.println();
185
+ UI.println("Shutting down...");
186
+ watcher.close();
187
+ process.exit(0);
188
+ };
189
+ process.on("SIGINT", shutdown);
190
+ process.on("SIGTERM", shutdown);
191
+ },
192
+ });
@@ -0,0 +1,19 @@
1
+ export declare const PluginEntryCommand: import("yargs").CommandModule<{}, {
2
+ tarball: string;
3
+ } & {
4
+ repo: string | undefined;
5
+ } & {
6
+ "download-url": string | undefined;
7
+ } & {
8
+ "signature-url": string | undefined;
9
+ } & {
10
+ "write-entry": string | undefined;
11
+ } & {
12
+ verified: boolean;
13
+ } & {
14
+ official: boolean;
15
+ } & {
16
+ changelog: string | undefined;
17
+ } & {
18
+ "--"?: string[];
19
+ }>;
@@ -0,0 +1,71 @@
1
+ import path from "path";
2
+ import { cmd } from "../cmd";
3
+ import { UI } from "../ui";
4
+ import { githubEntry, writeGithubEntry } from "../lib/market-entry";
5
+ export const PluginEntryCommand = cmd({
6
+ command: "entry <tarball>",
7
+ describe: "generate a SII-Holos/synergy-plugins registry entry JSON",
8
+ builder: (yargs) => yargs
9
+ .positional("tarball", {
10
+ type: "string",
11
+ describe: "path to the plugin .synergy-plugin.tgz tarball",
12
+ demandOption: true,
13
+ })
14
+ .option("repo", {
15
+ type: "string",
16
+ describe: "plugin GitHub repository URL",
17
+ })
18
+ .option("download-url", {
19
+ type: "string",
20
+ describe: "release asset URL for the .synergy-plugin.tgz",
21
+ })
22
+ .option("signature-url", {
23
+ type: "string",
24
+ describe: "release asset URL for the .sig file",
25
+ })
26
+ .option("write-entry", {
27
+ type: "string",
28
+ describe: "write or update a synergy-plugins plugins/<id>.json entry",
29
+ })
30
+ .option("verified", {
31
+ type: "boolean",
32
+ default: false,
33
+ describe: "mark the entry as verified",
34
+ })
35
+ .option("official", {
36
+ type: "boolean",
37
+ default: false,
38
+ describe: "mark the entry as official",
39
+ })
40
+ .option("changelog", {
41
+ type: "string",
42
+ describe: "version changelog",
43
+ }),
44
+ async handler(args) {
45
+ try {
46
+ const tarballPath = path.resolve(args.tarball);
47
+ const entry = githubEntry({
48
+ tarballPath,
49
+ repo: args.repo,
50
+ downloadUrl: args.downloadUrl,
51
+ signatureUrl: args.signatureUrl,
52
+ verified: Boolean(args.verified),
53
+ official: Boolean(args.official),
54
+ changelog: args.changelog,
55
+ });
56
+ const writeEntry = args.writeEntry;
57
+ if (writeEntry) {
58
+ const outputPath = path.resolve(writeEntry);
59
+ writeGithubEntry(outputPath, entry);
60
+ UI.println(`${UI.Style.TEXT_SUCCESS}✔${UI.Style.TEXT_NORMAL} Wrote GitHub registry entry ${outputPath}`);
61
+ }
62
+ else {
63
+ UI.println(JSON.stringify(entry, null, 2));
64
+ }
65
+ }
66
+ catch (error) {
67
+ UI.error(error instanceof Error ? error.message : String(error));
68
+ process.exitCode = 1;
69
+ }
70
+ },
71
+ });
@@ -0,0 +1,9 @@
1
+ export { PluginBuildCommand, buildPluginProject } from "./build";
2
+ export { PluginCreateCommand } from "./create";
3
+ export { PluginDevCommand } from "./dev";
4
+ export { PluginEntryCommand } from "./entry";
5
+ export { PluginPackCommand, packPluginProject } from "./pack";
6
+ export { PluginPublishMarketCommand } from "./publish-market";
7
+ export { PluginSignCommand, signPluginTarball } from "./sign";
8
+ export { PluginTestCommand } from "./test";
9
+ export { PluginValidateCommand, validatePluginProject } from "./validate";
@@ -0,0 +1,9 @@
1
+ export { PluginBuildCommand, buildPluginProject } from "./build";
2
+ export { PluginCreateCommand } from "./create";
3
+ export { PluginDevCommand } from "./dev";
4
+ export { PluginEntryCommand } from "./entry";
5
+ export { PluginPackCommand, packPluginProject } from "./pack";
6
+ export { PluginPublishMarketCommand } from "./publish-market";
7
+ export { PluginSignCommand, signPluginTarball } from "./sign";
8
+ export { PluginTestCommand } from "./test";
9
+ export { PluginValidateCommand, validatePluginProject } from "./validate";
@@ -0,0 +1,8 @@
1
+ import { type PluginManifest as PluginManifestType } from "@ericsanchezok/synergy-plugin";
2
+ export declare function readSourceManifest(pluginDir: string): PluginManifestType;
3
+ export declare function packPluginProject(pluginDir: string): string;
4
+ export declare const PluginPackCommand: import("yargs").CommandModule<{}, {
5
+ path: string | undefined;
6
+ } & {
7
+ "--"?: string[];
8
+ }>;
@@ -0,0 +1,64 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { PluginManifest } from "@ericsanchezok/synergy-plugin";
4
+ import { cmd } from "../cmd";
5
+ import { UI } from "../ui";
6
+ import { sha256File } from "../lib/crypto";
7
+ function formatSize(bytes) {
8
+ if (bytes < 1024)
9
+ return `${bytes} B`;
10
+ if (bytes < 1024 * 1024)
11
+ return `${(bytes / 1024).toFixed(1)} KB`;
12
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
13
+ }
14
+ function safePackageName(name) {
15
+ return name.replace(/[^a-zA-Z0-9_.-]/g, "-");
16
+ }
17
+ export function readSourceManifest(pluginDir) {
18
+ const manifestPath = path.join(pluginDir, "plugin.json");
19
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
20
+ return PluginManifest.parse(raw);
21
+ }
22
+ export function packPluginProject(pluginDir) {
23
+ const manifestPath = path.join(pluginDir, "plugin.json");
24
+ if (!fs.existsSync(manifestPath))
25
+ throw new Error(`No plugin.json found at ${manifestPath}`);
26
+ const manifest = readSourceManifest(pluginDir);
27
+ UI.println(`${UI.Style.TEXT_NORMAL_BOLD}Packing${UI.Style.TEXT_NORMAL} ${manifest.name} v${manifest.version}`);
28
+ const distDir = path.join(pluginDir, "dist");
29
+ if (!fs.existsSync(distDir))
30
+ throw new Error(`dist/ directory not found at ${distDir}. Run "synergy-plugin build" first.`);
31
+ if (!fs.existsSync(path.join(distDir, "plugin.json"))) {
32
+ throw new Error(`dist/plugin.json not found at ${distDir}. Run "synergy-plugin build" first.`);
33
+ }
34
+ const tgzName = `${safePackageName(manifest.name)}-${manifest.version}.synergy-plugin.tgz`;
35
+ const result = Bun.spawnSync(["tar", "-czf", tgzName, "-C", distDir, "."], { cwd: pluginDir });
36
+ if (result.exitCode !== 0) {
37
+ const stderr = new TextDecoder().decode(result.stderr);
38
+ throw new Error(`Failed to create tarball${stderr ? `: ${stderr}` : ""}`);
39
+ }
40
+ const tgzPath = path.join(pluginDir, tgzName);
41
+ const st = fs.statSync(tgzPath);
42
+ const integrity = sha256File(tgzPath);
43
+ UI.println(`${UI.Style.TEXT_SUCCESS}✔${UI.Style.TEXT_NORMAL} Packed: ${UI.Style.TEXT_HIGHLIGHT}${tgzName}${UI.Style.TEXT_NORMAL}`);
44
+ UI.println(` ${UI.Style.TEXT_DIM}Size:${UI.Style.TEXT_NORMAL} ${formatSize(st.size)}`);
45
+ UI.println(` ${UI.Style.TEXT_DIM}Integrity:${UI.Style.TEXT_NORMAL} sha256-${integrity}`);
46
+ return tgzPath;
47
+ }
48
+ export const PluginPackCommand = cmd({
49
+ command: "pack [path]",
50
+ describe: "package a built plugin into a .synergy-plugin.tgz",
51
+ builder: (yargs) => yargs.positional("path", {
52
+ type: "string",
53
+ describe: "path to plugin directory (defaults to cwd)",
54
+ }),
55
+ async handler(args) {
56
+ try {
57
+ packPluginProject(path.resolve(args.path ?? process.cwd()));
58
+ }
59
+ catch (error) {
60
+ UI.error(error instanceof Error ? error.message : String(error));
61
+ process.exitCode = 1;
62
+ }
63
+ },
64
+ });
@@ -0,0 +1,23 @@
1
+ export declare const PluginPublishMarketCommand: import("yargs").CommandModule<{}, {
2
+ tarball: string | undefined;
3
+ } & {
4
+ path: string | undefined;
5
+ } & {
6
+ repo: string | undefined;
7
+ } & {
8
+ "registry-dir": string | undefined;
9
+ } & {
10
+ "registry-repo": string;
11
+ } & {
12
+ "download-url": string | undefined;
13
+ } & {
14
+ "signature-url": string | undefined;
15
+ } & {
16
+ "skip-release-upload": boolean;
17
+ } & {
18
+ "no-pr": boolean;
19
+ } & {
20
+ changelog: string | undefined;
21
+ } & {
22
+ "--"?: string[];
23
+ }>;
@@ -0,0 +1,224 @@
1
+ import { $ } from "bun";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { cmd } from "../cmd";
6
+ import { UI } from "../ui";
7
+ import { buildPluginProject } from "./build";
8
+ import { packPluginProject } from "./pack";
9
+ import { signPluginTarball } from "./sign";
10
+ import { validatePluginProject } from "./validate";
11
+ import { githubEntry, githubRepoSlug, normalizeRepoUrl, releaseAssetUrl, readTarballManifest, writeGithubEntry, } from "../lib/market-entry";
12
+ const DEFAULT_REGISTRY_REPO = "https://github.com/SII-Holos/synergy-plugins.git";
13
+ async function commandExists(name) {
14
+ const result = await $ `which ${name}`.quiet().nothrow();
15
+ return result.exitCode === 0;
16
+ }
17
+ async function ghReady() {
18
+ if (!(await commandExists("gh")))
19
+ return false;
20
+ const result = await $ `gh auth status`.quiet().nothrow();
21
+ return result.exitCode === 0;
22
+ }
23
+ async function currentRepoUrl(cwd) {
24
+ const result = await $ `git remote get-url origin`.cwd(cwd).quiet().nothrow();
25
+ if (result.exitCode !== 0)
26
+ return undefined;
27
+ return normalizeRepoUrl(result.text().trim());
28
+ }
29
+ function defaultRegistryDir(pluginDir) {
30
+ const sibling = path.resolve(pluginDir, "..", "synergy-plugins");
31
+ if (fs.existsSync(sibling))
32
+ return sibling;
33
+ return path.join(os.homedir(), "projects", "synergy-plugins");
34
+ }
35
+ function safeArtifactName(name) {
36
+ return name.replace(/[^a-zA-Z0-9_.-]/g, "-");
37
+ }
38
+ function readTarballPackageName(tarballPath) {
39
+ const result = Bun.spawnSync(["tar", "-xOf", tarballPath, "package.json"], { stdout: "pipe", stderr: "pipe" });
40
+ if (result.exitCode !== 0)
41
+ return undefined;
42
+ const pkg = JSON.parse(new TextDecoder().decode(result.stdout));
43
+ return typeof pkg.name === "string" ? pkg.name : undefined;
44
+ }
45
+ function assertMarketplaceNaming(input) {
46
+ const packageName = readTarballPackageName(input.tarballPath);
47
+ if (!packageName) {
48
+ throw new Error("Marketplace publishing requires package.json inside the plugin tarball. Run `synergy-plugin build` and `synergy-plugin pack`.");
49
+ }
50
+ if (packageName !== input.manifest.name) {
51
+ throw new Error(`Marketplace publishing requires package.json name "${packageName}" to match plugin.json name "${input.manifest.name}".`);
52
+ }
53
+ const expectedArtifact = `${safeArtifactName(input.manifest.name)}-${input.manifest.version}.synergy-plugin.tgz`;
54
+ if (path.basename(input.tarballPath) !== expectedArtifact) {
55
+ throw new Error(`Marketplace publishing requires artifact name "${expectedArtifact}", got "${path.basename(input.tarballPath)}".`);
56
+ }
57
+ }
58
+ async function ensureRegistryCheckout(registryDir, registryRepo) {
59
+ if (fs.existsSync(path.join(registryDir, ".git")))
60
+ return;
61
+ fs.mkdirSync(path.dirname(registryDir), { recursive: true });
62
+ UI.println(`Cloning official plugin registry to ${registryDir}`);
63
+ const result = await $ `git clone ${registryRepo} ${registryDir}`.nothrow();
64
+ if (result.exitCode !== 0) {
65
+ throw new Error(`Failed to clone ${registryRepo}. Clone it manually or pass --registry-dir.`);
66
+ }
67
+ }
68
+ async function ensureGitHubReleaseAssets(input) {
69
+ if (input.skipUpload)
70
+ return;
71
+ if (!(await ghReady())) {
72
+ UI.println(`${UI.Style.TEXT_WARNING}gh is not authenticated; skipping GitHub Release upload.${UI.Style.TEXT_NORMAL}`);
73
+ return;
74
+ }
75
+ const repoSlug = githubRepoSlug(input.repo);
76
+ if (!repoSlug) {
77
+ UI.println(`${UI.Style.TEXT_WARNING}Could not derive GitHub owner/repo from ${input.repo}; skipping release upload.${UI.Style.TEXT_NORMAL}`);
78
+ return;
79
+ }
80
+ const tag = `v${input.version}`;
81
+ const view = await $ `gh release view ${tag} --repo ${repoSlug}`.quiet().nothrow();
82
+ if (view.exitCode === 0) {
83
+ await $ `gh release upload ${tag} ${input.tarballPath} ${input.signaturePath} --repo ${repoSlug} --clobber`;
84
+ return;
85
+ }
86
+ await $ `gh release create ${tag} ${input.tarballPath} ${input.signaturePath} --repo ${repoSlug} --title ${tag} --notes ${`Synergy plugin release ${tag}`}`;
87
+ }
88
+ async function runRegistryValidation(registryDir) {
89
+ await $ `bun install`.cwd(registryDir);
90
+ await $ `bun run validate`.cwd(registryDir);
91
+ await $ `bun run build-registry --check`.cwd(registryDir);
92
+ }
93
+ async function openRegistryPr(input) {
94
+ const branch = `publish/${input.pluginId}-${input.version}`;
95
+ await $ `git checkout -B ${branch}`.cwd(input.registryDir);
96
+ await $ `git add plugins/${input.pluginId}.json registry.json`.cwd(input.registryDir).nothrow();
97
+ const diff = await $ `git diff --cached --quiet`.cwd(input.registryDir).nothrow();
98
+ if (diff.exitCode === 0) {
99
+ UI.println(`${UI.Style.TEXT_DIM}No registry changes to commit.${UI.Style.TEXT_NORMAL}`);
100
+ return;
101
+ }
102
+ await $ `git commit -m ${`Add ${input.pluginId} ${input.version}`}`.cwd(input.registryDir);
103
+ if (input.noPr || !(await ghReady())) {
104
+ UI.println();
105
+ UI.println(`${UI.Style.TEXT_WARNING}Registry entry is ready, but PR was not opened automatically.${UI.Style.TEXT_NORMAL}`);
106
+ UI.println(` cd ${input.registryDir}`);
107
+ UI.println(` git push origin ${branch}`);
108
+ UI.println(` Open a PR against SII-Holos/synergy-plugins:main`);
109
+ return;
110
+ }
111
+ try {
112
+ await $ `git push -u origin ${branch}`.cwd(input.registryDir);
113
+ await $ `gh pr create --repo SII-Holos/synergy-plugins --base main --head ${branch} --title ${`Add ${input.pluginId} ${input.version}`} --body ${`Adds ${input.pluginId} ${input.version} to the official Synergy Plugin Marketplace.`}`.cwd(input.registryDir);
114
+ }
115
+ catch {
116
+ UI.println();
117
+ UI.println(`${UI.Style.TEXT_WARNING}Registry entry is committed, but the PR could not be opened automatically.${UI.Style.TEXT_NORMAL}`);
118
+ UI.println(` cd ${input.registryDir}`);
119
+ UI.println(` git push origin ${branch}`);
120
+ UI.println(` Open a PR against SII-Holos/synergy-plugins:main`);
121
+ }
122
+ }
123
+ export const PluginPublishMarketCommand = cmd({
124
+ command: "publish-market [tarball]",
125
+ describe: "prepare and open an official Synergy Plugin Marketplace PR",
126
+ builder: (yargs) => yargs
127
+ .positional("tarball", {
128
+ type: "string",
129
+ describe: "optional prebuilt .synergy-plugin.tgz tarball",
130
+ })
131
+ .option("path", {
132
+ type: "string",
133
+ describe: "plugin directory (defaults to cwd)",
134
+ })
135
+ .option("repo", {
136
+ type: "string",
137
+ describe: "plugin GitHub repository URL",
138
+ })
139
+ .option("registry-dir", {
140
+ type: "string",
141
+ describe: "local checkout path for SII-Holos/synergy-plugins",
142
+ })
143
+ .option("registry-repo", {
144
+ type: "string",
145
+ default: DEFAULT_REGISTRY_REPO,
146
+ describe: "registry repository to clone when --registry-dir does not exist",
147
+ })
148
+ .option("download-url", {
149
+ type: "string",
150
+ describe: "release asset URL for the .synergy-plugin.tgz",
151
+ })
152
+ .option("signature-url", {
153
+ type: "string",
154
+ describe: "release asset URL for the .sig file",
155
+ })
156
+ .option("skip-release-upload", {
157
+ type: "boolean",
158
+ default: false,
159
+ describe: "do not create/upload GitHub Release assets",
160
+ })
161
+ .option("no-pr", {
162
+ type: "boolean",
163
+ default: false,
164
+ describe: "prepare registry changes but do not open a PR",
165
+ })
166
+ .option("changelog", {
167
+ type: "string",
168
+ describe: "version changelog for the registry entry",
169
+ }),
170
+ async handler(args) {
171
+ try {
172
+ const pluginDir = path.resolve(args.path ?? process.cwd());
173
+ let tarballPath = args.tarball ? path.resolve(args.tarball) : undefined;
174
+ if (!tarballPath) {
175
+ await validatePluginProject(pluginDir, { runtimeDiscovery: true });
176
+ if (process.exitCode && process.exitCode !== 0)
177
+ throw new Error("Validation failed");
178
+ const built = await buildPluginProject(pluginDir);
179
+ if (!built)
180
+ throw new Error("Build failed");
181
+ tarballPath = packPluginProject(pluginDir);
182
+ }
183
+ const manifest = readTarballManifest(tarballPath);
184
+ assertMarketplaceNaming({ tarballPath, manifest });
185
+ await signPluginTarball(tarballPath);
186
+ const repo = normalizeRepoUrl(args.repo ?? (await currentRepoUrl(pluginDir)) ?? manifest.repository);
187
+ if (!repo)
188
+ throw new Error("Could not determine plugin GitHub repo. Pass --repo https://github.com/owner/repo.");
189
+ const signaturePath = `${tarballPath}.sig`;
190
+ await ensureGitHubReleaseAssets({
191
+ repo,
192
+ version: manifest.version,
193
+ tarballPath,
194
+ signaturePath,
195
+ skipUpload: Boolean(args["skip-release-upload"]),
196
+ });
197
+ const downloadUrl = args.downloadUrl ?? releaseAssetUrl(repo, manifest.version, path.basename(tarballPath));
198
+ const signatureUrl = args.signatureUrl ?? (downloadUrl ? `${downloadUrl}.sig` : undefined);
199
+ const entry = githubEntry({
200
+ tarballPath,
201
+ repo,
202
+ downloadUrl,
203
+ signatureUrl,
204
+ changelog: args.changelog,
205
+ });
206
+ const registryDir = path.resolve(args["registry-dir"] ?? defaultRegistryDir(pluginDir));
207
+ await ensureRegistryCheckout(registryDir, args["registry-repo"] ?? DEFAULT_REGISTRY_REPO);
208
+ const entryPath = path.join(registryDir, "plugins", `${entry.id}.json`);
209
+ writeGithubEntry(entryPath, entry);
210
+ await runRegistryValidation(registryDir);
211
+ await openRegistryPr({
212
+ registryDir,
213
+ pluginId: entry.id,
214
+ version: manifest.version,
215
+ noPr: Boolean(args["no-pr"]),
216
+ });
217
+ UI.println(`${UI.Style.TEXT_SUCCESS}✔${UI.Style.TEXT_NORMAL} Marketplace publishing request prepared for ${entry.id} v${manifest.version}`);
218
+ }
219
+ catch (error) {
220
+ UI.error(error instanceof Error ? error.message : String(error));
221
+ process.exitCode = 1;
222
+ }
223
+ },
224
+ });
@@ -0,0 +1,10 @@
1
+ export declare function signPluginTarball(tarballPath: string, options?: {
2
+ stdout?: boolean;
3
+ }): Promise<string>;
4
+ export declare const PluginSignCommand: import("yargs").CommandModule<{}, {
5
+ tarball: string;
6
+ } & {
7
+ stdout: boolean;
8
+ } & {
9
+ "--"?: string[];
10
+ }>;