@droposs/plugin-cli 0.6.1 → 0.7.0

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.
@@ -7,6 +7,7 @@ import {
7
7
  initPlugin,
8
8
  validateManifest,
9
9
  verifyPlugin,
10
+ startDevServer,
10
11
  } from "../dist/index.js";
11
12
  import { readFile } from "node:fs/promises";
12
13
  import path from "node:path";
@@ -52,7 +53,11 @@ function printUsage() {
52
53
  console.log(`Drop Plugin CLI (drop-plugin)
53
54
 
54
55
  Usage:
55
- drop-plugin init [dir] Initialize a new plugin from starter template
56
+ drop-plugin init [dir] Initialize a new plugin from template
57
+ (--template <starter|client-ui|metadata|store|runner|fullstack>)
58
+ (--id <id> --name <name> --author <author>)
59
+ drop-plugin dev [dir] Start hot-reloading dev server for live Drop testing
60
+ (--port <number>)
56
61
  drop-plugin build [dir] Bundle server/client entry points with esbuild and sign
57
62
  (--out-manifest <path> keeps the source manifest clean)
58
63
  drop-plugin sign [dir] Calculate SHA-256 digests and sign drop-plugin.json
@@ -85,7 +90,7 @@ async function main() {
85
90
  switch (command) {
86
91
  case "sign": {
87
92
  const outManifest = readFlagValue(args, "--out-manifest");
88
- const dir = positionalArg(args, [outManifest]) || ".";
93
+ const dir = positionalArg(args, [outManifest].filter(Boolean)) || ".";
89
94
  const res = await signPlugin(dir, undefined, true, { outManifest });
90
95
  console.log(
91
96
  `Signed bundle at ${dir}: ${res.fileCount} files verified (signature: ${res.signed ? "yes" : "no"})` +
@@ -104,21 +109,40 @@ async function main() {
104
109
  }
105
110
  case "build": {
106
111
  const outManifest = readFlagValue(args, "--out-manifest");
107
- const dir = positionalArg(args, [outManifest]) || ".";
112
+ const dir = positionalArg(args, [outManifest].filter(Boolean)) || ".";
108
113
  const res = await buildPlugin(dir, { outManifest });
109
114
  console.log(
110
115
  `Built plugin at ${dir} (server: ${res.serverBuilt ? "yes" : "no"}, client: ${res.clientBuilt ? "yes" : "no"})`,
111
116
  );
112
117
  break;
113
118
  }
119
+ case "dev": {
120
+ const portStr = readFlagValue(args, "--port");
121
+ const port = portStr ? parseInt(portStr, 10) : undefined;
122
+ const dir = positionalArg(args, [portStr].filter(Boolean)) || ".";
123
+ const server = await startDevServer(dir, { port });
124
+ console.log(`Drop plugin dev server running at ${server.url}`);
125
+ console.log(`Serving manifest: ${server.manifestUrl}`);
126
+ console.log(`Press Ctrl+C to stop.`);
127
+ await new Promise(() => {});
128
+ break;
129
+ }
114
130
  case "test": {
115
131
  const dir = args[0] || ".";
116
132
  await testPlugin(dir);
117
133
  break;
118
134
  }
119
135
  case "init": {
120
- const dir = args[0] || "my-drop-plugin";
121
- const res = await initPlugin(dir);
136
+ const template = readFlagValue(args, "--template");
137
+ const id = readFlagValue(args, "--id");
138
+ const name = readFlagValue(args, "--name");
139
+ const author = readFlagValue(args, "--author");
140
+ const dir =
141
+ positionalArg(
142
+ args,
143
+ [template, id, name, author].filter(Boolean),
144
+ ) || (id ? id : "my-drop-plugin");
145
+ const res = await initPlugin(dir, { template, id, name, author });
122
146
  console.log(
123
147
  `Initialized new Drop plugin '${res.id}' at ${res.targetPath}`,
124
148
  );
package/dist/builder.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as esbuild from "esbuild";
1
2
  export interface BuildOptions {
2
3
  minify?: boolean;
3
4
  sourcemap?: boolean;
@@ -5,8 +6,16 @@ export interface BuildOptions {
5
6
  signingKey?: string;
6
7
  /** Write derived manifest fields here instead of the source manifest. */
7
8
  outManifest?: string;
9
+ /** Watch for changes and rebuild incrementally. */
10
+ watch?: boolean;
11
+ /** Callback invoked on incremental rebuild completion (in watch mode). */
12
+ onRebuild?: (result: {
13
+ error?: Error;
14
+ }) => void;
8
15
  }
9
- export declare function buildPlugin(targetDir?: string, options?: BuildOptions): Promise<{
16
+ export interface BuildResult {
10
17
  serverBuilt: boolean;
11
18
  clientBuilt: boolean;
12
- }>;
19
+ contexts?: esbuild.BuildContext[];
20
+ }
21
+ export declare function buildPlugin(targetDir?: string, options?: BuildOptions): Promise<BuildResult>;
package/dist/builder.js CHANGED
@@ -1,13 +1,53 @@
1
1
  import * as esbuild from "esbuild";
2
2
  import path from "node:path";
3
- import { readFile, stat } from "node:fs/promises";
3
+ import { readFile, writeFile, stat } from "node:fs/promises";
4
+ import vue from "unplugin-vue/esbuild";
4
5
  import { signPlugin } from "./signer.js";
6
+ /**
7
+ * Global shim so bundled Vue SFCs and imports from "vue" resolve to globalThis.Vue (set by Drop Desktop)
8
+ * at runtime without requiring browser import maps.
9
+ */
10
+ const vueGlobalShimPlugin = {
11
+ name: "vue-global-shim",
12
+ setup(build) {
13
+ build.onResolve({ filter: /^vue$/ }, () => {
14
+ return { path: "virtual:vue", namespace: "vue-shim" };
15
+ });
16
+ build.onLoad({ filter: /.*/, namespace: "vue-shim" }, () => {
17
+ return {
18
+ contents: `
19
+ const v = globalThis.Vue || (typeof window !== "undefined" ? window.Vue : {});
20
+ export default v;
21
+ export const {
22
+ ref, reactive, computed, watch, watchEffect,
23
+ onMounted, onUnmounted, onUpdated, onBeforeMount, onBeforeUnmount,
24
+ h, defineComponent, nextTick, shallowRef, shallowReactive,
25
+ toRef, toRefs, isRef, isReactive, unref,
26
+ openBlock, createElementBlock, createBlock, createVNode, createCommentVNode,
27
+ createTextVNode, createElementVNode, toDisplayString, withDirectives,
28
+ vShow, vModelText, vModelCheckbox, vModelRadio, vModelSelect,
29
+ Fragment, Static, Comment, Text, Teleport, Suspense, KeepAlive,
30
+ renderSlot, resolveComponent, resolveDirective, normalizeClass, normalizeStyle,
31
+ withCtx, withModifiers, withKeys, renderList, pushScopeId, popScopeId
32
+ } = new Proxy(v, {
33
+ get: (target, prop) => {
34
+ const root = globalThis.Vue || (typeof window !== "undefined" ? window.Vue : undefined);
35
+ return root ? root[prop] : target[prop];
36
+ }
37
+ });
38
+ `,
39
+ loader: "js",
40
+ };
41
+ });
42
+ },
43
+ };
5
44
  export async function buildPlugin(targetDir = ".", options = {}) {
6
45
  const dir = path.resolve(process.cwd(), targetDir);
7
46
  const manifestPath = path.join(dir, "drop-plugin.json");
8
47
  const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
9
48
  let serverBuilt = false;
10
49
  let clientBuilt = false;
50
+ const contexts = [];
11
51
  // 1. Build Server Entry if source exists
12
52
  const serverSourceCandidates = [
13
53
  manifest.server?.source,
@@ -25,7 +65,7 @@ export async function buildPlugin(targetDir = ".", options = {}) {
25
65
  }
26
66
  const serverOutFile = path.resolve(dir, manifest.server?.entry ?? manifest.entry ?? "dist/src/index.js");
27
67
  if (serverEntrySource) {
28
- await esbuild.build({
68
+ const serverConfig = {
29
69
  entryPoints: [serverEntrySource],
30
70
  outfile: serverOutFile,
31
71
  bundle: true,
@@ -41,7 +81,15 @@ export async function buildPlugin(targetDir = ".", options = {}) {
41
81
  "h3",
42
82
  "pino",
43
83
  ],
44
- });
84
+ };
85
+ if (options.watch) {
86
+ const serverCtx = await esbuild.context(serverConfig);
87
+ await serverCtx.watch();
88
+ contexts.push(serverCtx);
89
+ }
90
+ else {
91
+ await esbuild.build(serverConfig);
92
+ }
45
93
  serverBuilt = true;
46
94
  }
47
95
  // 2. Build Client Entry if source exists
@@ -60,7 +108,14 @@ export async function buildPlugin(targetDir = ".", options = {}) {
60
108
  }
61
109
  const clientOutFile = path.resolve(dir, manifest.client?.entry ?? manifest.clientEntry ?? "dist/src/client.js");
62
110
  if (clientEntrySource) {
63
- await esbuild.build({
111
+ const clientPlugins = [
112
+ vueGlobalShimPlugin,
113
+ vue({
114
+ isProduction: true,
115
+ sourceMap: false,
116
+ }),
117
+ ];
118
+ const clientConfig = {
64
119
  entryPoints: [clientEntrySource],
65
120
  outfile: clientOutFile,
66
121
  bundle: true,
@@ -69,14 +124,33 @@ export async function buildPlugin(targetDir = ".", options = {}) {
69
124
  format: "esm",
70
125
  sourcemap: options.sourcemap ?? true,
71
126
  minify: options.minify ?? false,
127
+ plugins: clientPlugins,
72
128
  external: [
73
- "vue",
74
129
  "@droposs/plugin-sdk",
75
130
  "@droposs/plugin-sdk",
76
131
  "@drop/plugin-sdk",
77
132
  ],
78
- });
133
+ };
134
+ if (options.watch) {
135
+ const clientCtx = await esbuild.context(clientConfig);
136
+ await clientCtx.watch();
137
+ contexts.push(clientCtx);
138
+ }
139
+ else {
140
+ await esbuild.build(clientConfig);
141
+ }
79
142
  clientBuilt = true;
143
+ // Check if CSS was emitted and update manifest.client.css if necessary
144
+ const clientCssCandidate = clientOutFile.replace(/\.[^.]+$/, ".css");
145
+ if (await stat(clientCssCandidate).catch(() => null)) {
146
+ const relCss = path
147
+ .relative(dir, clientCssCandidate)
148
+ .replaceAll("\\", "/");
149
+ if (manifest.client && manifest.client.css !== relCss) {
150
+ manifest.client.css = relCss;
151
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
152
+ }
153
+ }
80
154
  }
81
155
  // 3. Automatically re-sign the plugin bundle after building
82
156
  if (options.sign !== false) {
@@ -84,5 +158,5 @@ export async function buildPlugin(targetDir = ".", options = {}) {
84
158
  outManifest: options.outManifest,
85
159
  });
86
160
  }
87
- return { serverBuilt, clientBuilt };
161
+ return { serverBuilt, clientBuilt, contexts: contexts.length > 0 ? contexts : undefined };
88
162
  }
@@ -0,0 +1,15 @@
1
+ import http from "node:http";
2
+ export interface DevServerOptions {
3
+ port?: number;
4
+ host?: string;
5
+ sign?: boolean;
6
+ }
7
+ export interface DevServerInstance {
8
+ server: http.Server;
9
+ port: number;
10
+ url: string;
11
+ manifestUrl: string;
12
+ close: () => Promise<void>;
13
+ stop: () => Promise<void>;
14
+ }
15
+ export declare function startDevServer(targetDir?: string, options?: DevServerOptions): Promise<DevServerInstance>;
@@ -0,0 +1,94 @@
1
+ import http from "node:http";
2
+ import path from "node:path";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { buildPlugin } from "./builder.js";
5
+ const MIME_TYPES = {
6
+ ".js": "application/javascript",
7
+ ".mjs": "application/javascript",
8
+ ".css": "text/css",
9
+ ".json": "application/json",
10
+ ".html": "text/html",
11
+ ".png": "image/png",
12
+ ".jpg": "image/jpeg",
13
+ ".svg": "image/svg+xml",
14
+ };
15
+ export async function startDevServer(targetDir = ".", options = {}) {
16
+ const dir = path.resolve(process.cwd(), targetDir);
17
+ const manifestPath = path.join(dir, "drop-plugin.json");
18
+ const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
19
+ const port = options.port ?? 4567;
20
+ const host = options.host ?? "localhost";
21
+ // 1. Start incremental watch build
22
+ const buildResult = await buildPlugin(dir, {
23
+ watch: true,
24
+ sign: options.sign ?? true,
25
+ });
26
+ // 2. Start HTTP static server with CORS
27
+ const server = http.createServer(async (req, res) => {
28
+ // CORS headers for Drop Desktop webview
29
+ res.setHeader("Access-Control-Allow-Origin", "*");
30
+ res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
31
+ res.setHeader("Access-Control-Allow-Headers", "*");
32
+ if (req.method === "OPTIONS") {
33
+ res.writeHead(204);
34
+ res.end();
35
+ return;
36
+ }
37
+ const urlPath = (req.url || "/").split("?")[0] || "/";
38
+ const filePath = path.join(dir, urlPath);
39
+ // Prevent directory traversal
40
+ const rel = path.relative(dir, filePath);
41
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
42
+ res.writeHead(403);
43
+ res.end("Forbidden");
44
+ return;
45
+ }
46
+ const fileStat = await stat(filePath).catch(() => null);
47
+ if (!fileStat || fileStat.isDirectory()) {
48
+ res.writeHead(404);
49
+ res.end("File not found");
50
+ return;
51
+ }
52
+ const ext = path.extname(filePath).toLowerCase();
53
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
54
+ try {
55
+ const content = await readFile(filePath);
56
+ res.writeHead(200, { "Content-Type": contentType });
57
+ res.end(content);
58
+ }
59
+ catch {
60
+ res.writeHead(500);
61
+ res.end("Internal Server Error");
62
+ }
63
+ });
64
+ await new Promise((resolve, reject) => {
65
+ server.listen(port, host, () => resolve());
66
+ server.on("error", reject);
67
+ });
68
+ const address = server.address();
69
+ const actualPort = typeof address === "object" && address ? address.port : port;
70
+ const url = `http://${host}:${actualPort}`;
71
+ const manifestUrl = `${url}/drop-plugin.json`;
72
+ const clientEntry = manifest.client?.entry ?? "dist/src/client.js";
73
+ console.log(`[drop-plugin] Dev server running at ${url}/`);
74
+ console.log(`[drop-plugin] Client bundle URL: ${url}/${clientEntry}`);
75
+ console.log(`[drop-plugin] In Drop Desktop, test this plugin via Extension Settings -> Load Dev Plugin:`);
76
+ console.log(` ${url}/${clientEntry}`);
77
+ console.log(`[drop-plugin] Watching for file changes... (Press Ctrl+C to stop)`);
78
+ const close = async () => {
79
+ if (buildResult.contexts) {
80
+ for (const ctx of buildResult.contexts) {
81
+ await ctx.dispose();
82
+ }
83
+ }
84
+ await new Promise((resolve) => server.close(() => resolve()));
85
+ };
86
+ return {
87
+ server,
88
+ port: actualPort,
89
+ url,
90
+ manifestUrl,
91
+ close,
92
+ stop: close,
93
+ };
94
+ }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./signer.js";
2
2
  export * from "./builder.js";
3
3
  export * from "./testRunner.js";
4
4
  export * from "./scaffolder.js";
5
+ export * from "./devServer.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from "./signer.js";
2
2
  export * from "./builder.js";
3
3
  export * from "./testRunner.js";
4
4
  export * from "./scaffolder.js";
5
+ export * from "./devServer.js";
@@ -1,8 +1,18 @@
1
+ export type TemplateType = "starter" | "client-ui" | "metadata" | "store" | "runner" | "fullstack";
1
2
  export interface InitOptions {
2
3
  id?: string;
3
4
  name?: string;
4
5
  author?: string;
6
+ template?: string;
5
7
  }
8
+ /**
9
+ * Rewrites a freshly copied template so its `@drop-oss` monorepo specifiers
10
+ * and `workspace:*` dependencies point at the scope/version declared in the
11
+ * template's `.sdk-scope.json`. Without this a scaffolded repo keeps
12
+ * `workspace:*` and cannot install outside the SDK monorepo.
13
+ */
14
+ export declare function applySdkScope(targetPath: string): Promise<boolean>;
15
+ export declare function resolveTemplateFolder(templateName?: string): string;
6
16
  export declare function initPlugin(targetDir: string, options?: InitOptions): Promise<{
7
17
  targetPath: string;
8
18
  id: string;
@@ -1,12 +1,163 @@
1
1
  import path from "node:path";
2
- import { cp, mkdir, readFile, writeFile, stat } from "node:fs/promises";
2
+ import { cp, mkdir, readFile, writeFile, stat, readdir, realpath, } from "node:fs/promises";
3
+ /** npm scopes the templates may resolve the SDK/CLI from. */
4
+ const SDK_SCOPES = ["@drop-oss", "@droposs", "@drop"];
5
+ const SKIP_DIRS = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ "dist",
9
+ "dist-package",
10
+ "dist-packages",
11
+ ]);
12
+ const TEXT_EXT = new Set([
13
+ ".ts",
14
+ ".tsx",
15
+ ".js",
16
+ ".mjs",
17
+ ".cjs",
18
+ ".json",
19
+ ".md",
20
+ ".yml",
21
+ ".yaml",
22
+ ]);
23
+ function parseSdkScope(text) {
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(text);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ const { sdk, sdkVersion, cliVersion } = parsed;
32
+ if (typeof sdk !== "string" ||
33
+ typeof sdkVersion !== "string" ||
34
+ typeof cliVersion !== "string" ||
35
+ !SDK_SCOPES.includes(sdk)) {
36
+ return null;
37
+ }
38
+ return { sdk, sdkVersion, cliVersion };
39
+ }
40
+ function rewriteSpecifiers(text, scope) {
41
+ let out = text;
42
+ for (const other of SDK_SCOPES) {
43
+ if (other === scope)
44
+ continue;
45
+ out = out
46
+ .replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
47
+ .replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
48
+ }
49
+ return out;
50
+ }
51
+ function rewritePackageDeps(text, config) {
52
+ let pkg;
53
+ try {
54
+ pkg = JSON.parse(text);
55
+ }
56
+ catch {
57
+ return text;
58
+ }
59
+ for (const sectionName of [
60
+ "dependencies",
61
+ "devDependencies",
62
+ "peerDependencies",
63
+ "optionalDependencies",
64
+ ]) {
65
+ const section = pkg[sectionName];
66
+ if (!section)
67
+ continue;
68
+ for (const dep of Object.keys(section)) {
69
+ const kind = dep.endsWith("/plugin-sdk")
70
+ ? "plugin-sdk"
71
+ : dep.endsWith("/plugin-cli")
72
+ ? "plugin-cli"
73
+ : null;
74
+ if (!kind)
75
+ continue;
76
+ delete section[dep];
77
+ section[`${config.sdk}/${kind}`] =
78
+ kind === "plugin-sdk" ? config.sdkVersion : config.cliVersion;
79
+ }
80
+ }
81
+ return JSON.stringify(pkg, null, 2) + "\n";
82
+ }
83
+ async function walkFiles(dir, files = [], visited = new Set()) {
84
+ const real = await realpath(dir).catch(() => dir);
85
+ if (visited.has(real))
86
+ return files;
87
+ visited.add(real);
88
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
89
+ if (SKIP_DIRS.has(entry.name))
90
+ continue;
91
+ if (entry.isSymbolicLink())
92
+ continue;
93
+ const full = path.join(dir, entry.name);
94
+ if (entry.isDirectory())
95
+ await walkFiles(full, files, visited);
96
+ else
97
+ files.push(full);
98
+ }
99
+ return files;
100
+ }
101
+ /**
102
+ * Rewrites a freshly copied template so its `@drop-oss` monorepo specifiers
103
+ * and `workspace:*` dependencies point at the scope/version declared in the
104
+ * template's `.sdk-scope.json`. Without this a scaffolded repo keeps
105
+ * `workspace:*` and cannot install outside the SDK monorepo.
106
+ */
107
+ export async function applySdkScope(targetPath) {
108
+ const raw = await readFile(path.join(targetPath, ".sdk-scope.json"), "utf-8").catch(() => null);
109
+ if (raw === null)
110
+ return false;
111
+ const config = parseSdkScope(raw);
112
+ if (!config)
113
+ return false;
114
+ for (const file of await walkFiles(targetPath)) {
115
+ if (path.basename(file).match(/lock\.(json|yaml|lockb)$/))
116
+ continue;
117
+ if (!TEXT_EXT.has(path.extname(file)))
118
+ continue;
119
+ const before = await readFile(file, "utf-8");
120
+ let after = rewriteSpecifiers(before, config.sdk);
121
+ if (path.basename(file) === "package.json") {
122
+ after = rewritePackageDeps(after, config);
123
+ }
124
+ if (after !== before)
125
+ await writeFile(file, after);
126
+ }
127
+ return true;
128
+ }
129
+ export function resolveTemplateFolder(templateName = "starter") {
130
+ const norm = templateName.replace(/^template-/, "").toLowerCase();
131
+ switch (norm) {
132
+ case "client-ui":
133
+ case "ui":
134
+ case "client":
135
+ return "template-client-ui";
136
+ case "metadata":
137
+ return "template-metadata";
138
+ case "store":
139
+ return "template-store";
140
+ case "runner":
141
+ return "template-runner";
142
+ case "fullstack":
143
+ case "full":
144
+ return "template-fullstack";
145
+ case "starter":
146
+ case "default":
147
+ return "starter-plugin";
148
+ default:
149
+ throw new Error(`Unknown template "${templateName}". Available templates: starter, client-ui, metadata, store, runner, fullstack`);
150
+ }
151
+ }
3
152
  export async function initPlugin(targetDir, options = {}) {
4
153
  const targetPath = path.resolve(process.cwd(), targetDir);
5
154
  await mkdir(targetPath, { recursive: true });
155
+ const folder = resolveTemplateFolder(options.template);
6
156
  const candidates = [
7
- path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates/starter-plugin"),
8
- path.resolve(path.dirname(new URL(import.meta.url).pathname), "../templates/starter-plugin"),
9
- path.resolve(process.cwd(), "templates/starter-plugin"),
157
+ path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../templates", folder),
158
+ path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../templates", folder),
159
+ path.resolve(path.dirname(new URL(import.meta.url).pathname), "../templates", folder),
160
+ path.resolve(process.cwd(), "templates", folder),
10
161
  ];
11
162
  let templateDir = null;
12
163
  for (const cand of candidates) {
@@ -16,7 +167,7 @@ export async function initPlugin(targetDir, options = {}) {
16
167
  }
17
168
  }
18
169
  if (!templateDir) {
19
- throw new Error("Starter plugin template directory not found");
170
+ throw new Error(`Plugin template directory for "${folder}" not found`);
20
171
  }
21
172
  await cp(templateDir, targetPath, {
22
173
  recursive: true,
@@ -25,6 +176,9 @@ export async function initPlugin(targetDir, options = {}) {
25
176
  return basename !== "node_modules" && basename !== "dist";
26
177
  },
27
178
  });
179
+ // Point the scaffolded repo at the configured npm scope/version instead of
180
+ // the monorepo's `workspace:*` links.
181
+ await applySdkScope(targetPath);
28
182
  const pluginId = options.id ||
29
183
  path
30
184
  .basename(targetPath)
package/dist/signer.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SIGNATURE_VERSION } from "@drop-oss/plugin-sdk";
1
+ import { SIGNATURE_VERSION, type PluginManifest } from "@drop-oss/plugin-sdk";
2
2
  /** Current signature scheme; re-exported for backwards compatibility. */
3
3
  export { SIGNATURE_VERSION };
4
4
  /**
@@ -39,6 +39,13 @@ export interface VerifyResult {
39
39
  export declare function verifyPlugin(targetDir: string, signingKey?: string, options?: {
40
40
  allowUnsigned?: boolean;
41
41
  }): Promise<VerifyResult>;
42
+ /**
43
+ * Validate the `client.sidecars` declaration against the bundle contents:
44
+ * each target path must resolve inside the bundle, point to a present regular
45
+ * file covered by the `files` checksums, and cite a matching SHA-256 of the
46
+ * binary. Every sidecar name must be allowlisted in `client.commands`, and
47
+ */
48
+ export declare function verifySidecars(bundleDir: string, manifest: PluginManifest & Record<string, unknown>, files: string[], present: Set<string>, errors: string[]): Promise<void>;
42
49
  export declare function packPlugin(targetDir: string, outputDir?: string): Promise<{
43
50
  packagePath: string;
44
51
  id: string;
package/dist/signer.js CHANGED
@@ -91,7 +91,6 @@ let cachedErrors = [];
91
91
  export async function validateManifest(manifest) {
92
92
  if (!cachedValidator) {
93
93
  const schema = await loadSchema();
94
- // @ts-ignore
95
94
  const AjvClass = Ajv.default ?? Ajv;
96
95
  const ajv = new AjvClass({ allErrors: true, strict: false });
97
96
  const compiled = ajv.compile(schema);
@@ -189,6 +188,13 @@ export async function signPlugin(targetDir, signingKey, validate = true, options
189
188
  if (!validation.valid) {
190
189
  throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
191
190
  }
191
+ const files = await listFiles(bundleDir);
192
+ const present = new Set(files);
193
+ const sidecarErrors = [];
194
+ await verifySidecars(bundleDir, manifest, files, present, sidecarErrors);
195
+ if (sidecarErrors.length > 0) {
196
+ throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
197
+ }
192
198
  }
193
199
  const derived = await deriveManifest(bundleDir, manifest, signingKey);
194
200
  const outPath = options.outManifest
@@ -246,6 +252,7 @@ export async function verifyPlugin(targetDir, signingKey, options = {}) {
246
252
  errors.push("bundle contains multiple code files but no 'files' checksums; refusing unverified imports");
247
253
  }
248
254
  const entry = manifest.entry ?? manifest.server?.entry ?? manifest.client?.entry;
255
+ await verifySidecars(bundleDir, manifest, files, present, errors);
249
256
  if (entry) {
250
257
  const entryPath = path.resolve(bundleDir, entry);
251
258
  if (!isInside(bundleDir, entryPath)) {
@@ -320,6 +327,72 @@ async function resolveSignedPayload(bundleDir, manifest, filesAggregate, entry)
320
327
  error: "legacy signature has neither file checksums nor an entry checksum",
321
328
  };
322
329
  }
330
+ /**
331
+ * Validate the `client.sidecars` declaration against the bundle contents:
332
+ * each target path must resolve inside the bundle, point to a present regular
333
+ * file covered by the `files` checksums, and cite a matching SHA-256 of the
334
+ * binary. Every sidecar name must be allowlisted in `client.commands`, and
335
+ */
336
+ export async function verifySidecars(bundleDir, manifest, files, present, errors) {
337
+ const sidecars = manifest.client?.sidecars;
338
+ if (sidecars === undefined)
339
+ return;
340
+ if (!Array.isArray(sidecars) || sidecars.length === 0) {
341
+ errors.push("client.sidecars must be a non-empty array when declared");
342
+ return;
343
+ }
344
+ const commands = new Set(Array.isArray(manifest.client?.commands) ? manifest.client.commands : []);
345
+ for (const [idx, sidecar] of sidecars.entries()) {
346
+ const label = `client.sidecars[${idx}]`;
347
+ if (typeof sidecar !== "object" ||
348
+ sidecar === null ||
349
+ typeof sidecar.name !== "string" ||
350
+ !Array.isArray(sidecar.targets)) {
351
+ errors.push(`${label}: expected { name: string, targets: array }`);
352
+ continue;
353
+ }
354
+ const { name, targets } = sidecar;
355
+ if (!commands.has(name)) {
356
+ errors.push(`${label}: sidecar name '${name}' must be allowlisted in client.commands`);
357
+ }
358
+ const seenTargets = new Set();
359
+ for (const [tIdx, target] of targets.entries()) {
360
+ const tLabel = `${label}.targets[${tIdx}]`;
361
+ if (!target || typeof target !== "object") {
362
+ errors.push(`${tLabel}: expected target object`);
363
+ continue;
364
+ }
365
+ if (typeof target.os !== "string" || typeof target.arch !== "string") {
366
+ errors.push(`${tLabel}: expected string os and arch`);
367
+ continue;
368
+ }
369
+ const key = `${target.os}-${target.arch}`;
370
+ if (seenTargets.has(key)) {
371
+ errors.push(`${tLabel}: duplicate target '${key}' (only one binary per os+arch for sidecar '${name}')`);
372
+ continue;
373
+ }
374
+ seenTargets.add(key);
375
+ if (typeof target.path !== "string" ||
376
+ path.isAbsolute(target.path) ||
377
+ !isInside(bundleDir, path.resolve(bundleDir, target.path))) {
378
+ errors.push(`${tLabel}: path must be a bundle-relative path`);
379
+ continue;
380
+ }
381
+ if (isBundleCodeFile(target.path)) {
382
+ errors.push(`${tLabel}: 'sidecars' paths must not be JavaScript code files`);
383
+ continue;
384
+ }
385
+ if (!present.has(target.path)) {
386
+ errors.push(`${tLabel}: declared sidecar file missing: ${target.path}`);
387
+ continue;
388
+ }
389
+ const digest = sha256Hex(await readFile(path.join(bundleDir, target.path)));
390
+ if (target.sha256 !== digest) {
391
+ errors.push(`${tLabel}: sha256 mismatch for ${target.path} (declared ${target.sha256}, actual ${digest})`);
392
+ }
393
+ }
394
+ }
395
+ }
323
396
  export async function packPlugin(targetDir, outputDir) {
324
397
  const resolvedPath = path.resolve(process.cwd(), targetDir);
325
398
  const bundleDir = await realpath(resolvedPath).catch(() => null);
@@ -332,6 +405,13 @@ export async function packPlugin(targetDir, outputDir) {
332
405
  if (!validation.valid) {
333
406
  throw new Error(`Manifest validation failed against schema:\n ${validation.errors.join("\n ")}`);
334
407
  }
408
+ const files = await listFiles(bundleDir);
409
+ const present = new Set(files);
410
+ const sidecarErrors = [];
411
+ await verifySidecars(bundleDir, rawManifest, files, present, sidecarErrors);
412
+ if (sidecarErrors.length > 0) {
413
+ throw new Error(`Sidecar validation failed:\n ${sidecarErrors.join("\n ")}`);
414
+ }
335
415
  // Derive the signed manifest in memory: packing must not dirty the source
336
416
  // tree, the derived fields live in the archive only.
337
417
  const { manifest } = await deriveManifest(bundleDir, rawManifest);
@@ -344,7 +424,6 @@ export async function packPlugin(targetDir, outputDir) {
344
424
  ? path.resolve(process.cwd(), outputDir)
345
425
  : path.join(bundleDir, "dist-package");
346
426
  await mkdir(outDir, { recursive: true });
347
- const files = await listFiles(bundleDir);
348
427
  const bundleMap = {};
349
428
  for (const rel of files) {
350
429
  const content = await readFile(path.join(bundleDir, rel));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droposs/plugin-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Drop Plugin build, test, signing, and packaging CLI for Drop OSS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,8 @@
50
50
  "dependencies": {
51
51
  "ajv": "^8.20.0",
52
52
  "esbuild": "^0.28.2",
53
- "@droposs/plugin-sdk": "0.6.1"
53
+ "unplugin-vue": "^8.0.0",
54
+ "vue": "^3.5.42",
55
+ "@droposs/plugin-sdk": "0.7.0"
54
56
  }
55
57
  }