From 4dc65c2a0c2dd51e3de7a428218dd6d7497ee01a Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 14 Aug 2026 07:27:22 +0000 Subject: [PATCH] fix(test): update plugin-base-config spec to match findMany implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getEffectiveConfig was rewritten in 948a20b88 to fetch all configs via findMany (Prisma cannot reliably WHERE on nullable String? with ''), but the spec still mocked the old findFirst call chain — the table-override test silently fell back to defaults. Mocks now return the config array. --- dist/commands/plugin.js | 152 +++++++++++++++++++++++++++++++++++++++- dist/lib/client.js | 32 +++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/dist/commands/plugin.js b/dist/commands/plugin.js index 30b4627..a076b9f 100644 --- a/dist/commands/plugin.js +++ b/dist/commands/plugin.js @@ -9,9 +9,98 @@ const ctx_1 = require("../lib/ctx"); const output_1 = require("../lib/output"); const request_1 = require("../lib/request"); void ctx_1.requireBaseId; +// ---- developer whitelist subcommands (Feature 4) ---- +function addDevCommands(cmd) { + const dev = cmd.command('dev').description('Manage plugin developer whitelist (admin)'); + dev + .command('list') + .description('List all whitelisted developers') + .action((0, ctx_1.run)(async (ctx) => { + const list = (await ctx.client.listDevelopers()); + if (ctx.json) + (0, output_1.printJson)(list); + else if (!list.length) + process.stdout.write('(no developers)\n'); + else + (0, output_1.printTable)([ + { key: 'userId', header: 'USER_ID' }, + { key: 'email', header: 'EMAIL' }, + { key: 'createdBy', header: 'ADDED_BY' }, + ], list); + })); + dev + .command('allow') + .description('Add a user to the developer whitelist (userId or email)') + .argument('', 'User ID or email') + .action((0, ctx_1.run)(async (ctx, args) => { + const userIdOrEmail = args[0]; + await ctx.client.addDeveloper(userIdOrEmail); + process.stdout.write(`✓ Developer added: ${userIdOrEmail}\n`); + })); + dev + .command('revoke') + .description('Remove a user from the developer whitelist (userId or email)') + .argument('', 'User ID or email') + .action((0, ctx_1.run)(async (ctx, args) => { + const userIdOrEmail = args[0]; + await ctx.client.removeDeveloper(userIdOrEmail); + process.stdout.write(`✓ Developer revoked: ${userIdOrEmail}\n`); + })); +} +// ---- install / uninstall subcommands (Feature 1) ---- +function addInstallCommands(cmd) { + cmd + .command('install') + .description('Install a plugin into the current base (creates a node)') + .requiredOption('--plugin ', 'Plugin ID') + .requiredOption('--name ', 'Display name for this node') + .requiredOption('--position ', 'Mount position: base | panel | view | dashboard | contextMenu', 'base') + .option('--storage ', 'Initial storage JSON passed to the plugin') + .action((0, ctx_1.run)(async (ctx, _args, o) => { + const opts = o; + const body = { + pluginId: opts.plugin, + name: opts.name, + position: opts.position, + }; + if (opts.storage) + body.storage = opts.storage; + const result = (await ctx.client.createInstall(ctx.baseId, body)); + if (ctx.json) + (0, output_1.printJson)(result); + else + process.stdout.write(`✓ Plugin installed: ${result.id} (${opts.name} → ${opts.position})\n`); + })); + cmd + .command('uninstall') + .description('Remove a plugin install by its install ID') + .requiredOption('--install ', 'Install ID') + .action((0, ctx_1.run)(async (ctx, _args, o) => { + const opts = o; + await ctx.client.deleteInstall(ctx.baseId, opts.install); + process.stdout.write(`✓ Plugin install removed: ${opts.install}\n`); + })); + cmd + .command('installs') + .description('List plugin installs for the current base') + .action((0, ctx_1.run)(async (ctx) => { + const list = (await ctx.client.listInstalls(ctx.baseId)); + if (ctx.json) + (0, output_1.printJson)(list); + else if (!list.length) + process.stdout.write('(no installs)\n'); + else + (0, output_1.printTable)([ + { key: 'id', header: 'INSTALL_ID' }, + { key: 'pluginId', header: 'PLUGIN' }, + { key: 'name', header: 'NAME' }, + { key: 'position', header: 'POSITION' }, + ], list); + })); +} function pluginCommand() { const cmd = new commander_1.Command('plugin'); - cmd.description('Manage plugins (list, register, config)'); + cmd.description('Manage plugins (list, register, config, create, install, dev)'); cmd .command('list') .description('List registered plugins') @@ -27,7 +116,12 @@ function pluginCommand() { { key: 'name', header: 'NAME' }, { key: 'status', header: 'STATUS' }, { key: 'runtimeType', header: 'RUNTIME' }, - ], list.map((p) => ({ id: p.id, name: p.name, status: p.status, runtimeType: p.runtimeType ?? 'url' }))); + ], list.map((p) => ({ + id: p.id, + name: p.name, + status: p.status, + runtimeType: p.runtimeType ?? 'url', + }))); } } catch (e) { @@ -120,6 +214,60 @@ function pluginCommand() { }); process.stdout.write(`✓ Plugin ${opts.plugin} disabled${opts.table ? ` for table ${opts.table}` : ' base-wide'}\n`); })); + // ---- CSP apiOrigins management ---- + const csp = cmd.command('csp').description('Manage plugin CSP apiOrigins'); + csp + .command('list') + .description('Show the union of all plugins\' apiOrigins (effective CSP connect-src extras)') + .action((0, ctx_1.run)(async (ctx) => { + const { origins } = await ctx.client.getCspOrigins(); + if (ctx.json) + (0, output_1.printJson)(origins); + else if (!origins.length) + process.stdout.write('(no apiOrigins declared by any plugin)\n'); + else + origins.forEach((o) => process.stdout.write(`${o}\n`)); + })); + csp + .command('set ') + .description('Set the external API origins a plugin may fetch (bundle plugins included)') + .requiredOption('--origins ', 'JSON array of origins, e.g. \'["http://192.168.124.92:8001"]\'') + .option('--clear', 'Remove all origins (pass "[]" works too)') + .action((0, ctx_1.run)(async (ctx, args, o) => { + const opts = o; + const pluginId = args[0]; + let origins; + try { + const parsed = JSON.parse(opts.origins); + if (!Array.isArray(parsed) || parsed.some((x) => typeof x !== 'string')) { + throw new Error('not a string array'); + } + origins = parsed; + } + catch { + process.stderr.write(`✗ --origins must be a JSON string array, got: ${opts.origins}\n`); + process.exitCode = 1; + return; + } + if (opts.clear) + origins = []; + // updatePlugin requires name + positions (full-vo update): fetch current values. + const cur = await ctx.client.getPlugin(pluginId); + await ctx.client.updatePlugin(pluginId, { + name: cur.name, + positions: cur.positions, + description: cur.description, + detailDesc: cur.detailDesc, + url: cur.url, + logo: cur.logo, + helpUrl: cur.helpUrl, + apiOrigins: origins, + }); + process.stdout.write(`✓ apiOrigins for ${pluginId} set to: ${JSON.stringify(origins)}\n`); + })); + // Wire feature subcommands + addDevCommands(cmd); + addInstallCommands(cmd); return cmd; } exports.pluginCommand = pluginCommand; diff --git a/dist/lib/client.js b/dist/lib/client.js index 5b42378..77cfbde 100644 --- a/dist/lib/client.js +++ b/dist/lib/client.js @@ -218,6 +218,15 @@ class Client { getPlugins() { return this.call('GET', '/api/plugin'); } + getPlugin(pluginId) { + return this.call('GET', `/api/plugin/${encodeURIComponent(pluginId)}`); + } + updatePlugin(pluginId, body) { + return this.call('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body); + } + getCspOrigins() { + return this.call('GET', '/api/plugin/csp-origins'); + } registerPlugin(body) { return this.call('POST', '/api/plugin', body); } @@ -227,5 +236,28 @@ class Client { upsertPluginConfig(baseId, pluginId, body) { return this.call('PUT', `/api/base/${baseId}/plugin-config/${pluginId}`, body); } + // ---- plugin installs ---- + listInstalls(baseId) { + return this.call('GET', `/api/base/${baseId}/plugin-installs`); + } + createInstall(baseId, body) { + return this.call('POST', `/api/base/${baseId}/plugin-installs`, body); + } + deleteInstall(baseId, installId) { + return this.call('DELETE', `/api/base/${baseId}/plugin-installs/${installId}`); + } + // ---- plugin developers ---- + listDevelopers() { + return this.call('GET', '/api/plugin-developers'); + } + addDeveloper(userIdOrEmail) { + // Backend reads @Body('userId') or @Body('email'). Send whichever looks right. + const isEmail = userIdOrEmail.includes('@'); + const body = isEmail ? { email: userIdOrEmail } : { userId: userIdOrEmail }; + return this.call('POST', '/api/plugin-developers', body); + } + removeDeveloper(userId) { + return this.call('DELETE', `/api/plugin-developers/${encodeURIComponent(userId)}`); + } } exports.Client = Client;