- CLI: plugin dev list/allow/revoke (userId or email) - Backend: PluginDeveloperService.addDeveloper is now idempotent (returns existing record instead of unique-constraint 500) - Backend: removeDeveloper silently no-ops on P2025 (not found) - Controller: route api/plugin-developers (avoids :pluginId collision), @DisabledPermission so admin check is the sole gate
287 lines
9.0 KiB
TypeScript
287 lines
9.0 KiB
TypeScript
/**
|
|
* `inkreach-teable plugin` — plugin management commands.
|
|
*/
|
|
import { Command } from 'commander';
|
|
import { run, requireBaseId } from '../lib/ctx';
|
|
import { printJson, printTable } from '../lib/output';
|
|
import { ApiError } from '../lib/request';
|
|
|
|
void requireBaseId;
|
|
|
|
// ---- developer whitelist subcommands (Feature 4) ----
|
|
function addDevCommands(cmd: Command) {
|
|
const dev = cmd.command('dev').description('Manage plugin developer whitelist (admin)');
|
|
|
|
dev
|
|
.command('list')
|
|
.description('List all whitelisted developers')
|
|
.action(
|
|
run(async (ctx) => {
|
|
const list = (await ctx.client.listDevelopers()) as Array<{
|
|
userId: string;
|
|
email: string;
|
|
createdBy: string;
|
|
}>;
|
|
if (ctx.json) printJson(list);
|
|
else if (!list.length) process.stdout.write('(no developers)\n');
|
|
else
|
|
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('<userIdOrEmail>', 'User ID or email')
|
|
.action(
|
|
run(async (ctx, args) => {
|
|
const userIdOrEmail = args[0] as string;
|
|
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('<userIdOrEmail>', 'User ID or email')
|
|
.action(
|
|
run(async (ctx, args) => {
|
|
const userIdOrEmail = args[0] as string;
|
|
await ctx.client.removeDeveloper(userIdOrEmail);
|
|
process.stdout.write(`✓ Developer revoked: ${userIdOrEmail}\n`);
|
|
})
|
|
);
|
|
}
|
|
|
|
// ---- install / uninstall subcommands (Feature 1) ----
|
|
function addInstallCommands(cmd: Command) {
|
|
cmd
|
|
.command('install')
|
|
.description('Install a plugin into the current base (creates a node)')
|
|
.requiredOption('--plugin <id>', 'Plugin ID')
|
|
.requiredOption('--name <name>', 'Display name for this node')
|
|
.requiredOption(
|
|
'--position <pos>',
|
|
'Mount position: base | panel | view | dashboard | contextMenu',
|
|
'base'
|
|
)
|
|
.option('--storage <json>', 'Initial storage JSON passed to the plugin')
|
|
.action(
|
|
run(async (ctx, _args, o) => {
|
|
const opts = o as { plugin: string; name: string; position: string; storage?: string };
|
|
const body: Record<string, unknown> = {
|
|
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)) as { id: string };
|
|
if (ctx.json) 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 <id>', 'Install ID')
|
|
.action(
|
|
run(async (ctx, _args, o) => {
|
|
const opts = o as { install: string };
|
|
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(
|
|
run(async (ctx) => {
|
|
const list = (await ctx.client.listInstalls(ctx.baseId!)) as Array<{
|
|
id: string;
|
|
pluginId: string;
|
|
name: string;
|
|
position: string;
|
|
}>;
|
|
if (ctx.json) printJson(list);
|
|
else if (!list.length) process.stdout.write('(no installs)\n');
|
|
else
|
|
printTable(
|
|
[
|
|
{ key: 'id', header: 'INSTALL_ID' },
|
|
{ key: 'pluginId', header: 'PLUGIN' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'position', header: 'POSITION' },
|
|
],
|
|
list
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
export function pluginCommand(): Command {
|
|
const cmd = new Command('plugin');
|
|
cmd.description('Manage plugins (list, register, config, create, install, dev)');
|
|
|
|
cmd
|
|
.command('list')
|
|
.description('List registered plugins')
|
|
.action(
|
|
run(async (ctx) => {
|
|
try {
|
|
const list = (await ctx.client.getPlugins()) as Array<{
|
|
id: string;
|
|
name: string;
|
|
status: string;
|
|
runtimeType?: string;
|
|
}>;
|
|
if (ctx.json) {
|
|
printJson(list);
|
|
} else {
|
|
printTable(
|
|
[
|
|
{ key: 'id', header: 'ID' },
|
|
{ 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',
|
|
}))
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 401) {
|
|
process.stderr.write('Unauthorized — log in via "auth login" or provide --token\n');
|
|
process.exit(1);
|
|
}
|
|
throw e;
|
|
}
|
|
})
|
|
);
|
|
|
|
cmd
|
|
.command('register')
|
|
.description('Register a plugin by URL')
|
|
.requiredOption('--url <url>', 'Plugin URL')
|
|
.requiredOption('--name <name>', 'Plugin name')
|
|
.option('--description <desc>', 'Plugin description')
|
|
.option('--logo <url>', 'Logo URL')
|
|
.option('--positions <json>', 'Positions array, e.g. \'["panel"]\'', '["panel"]')
|
|
.action(
|
|
run(async (ctx, _args, o) => {
|
|
const opts = o as {
|
|
url: string;
|
|
name: string;
|
|
description?: string;
|
|
logo?: string;
|
|
positions: string;
|
|
};
|
|
const positions = JSON.parse(opts.positions);
|
|
const created = await ctx.client.registerPlugin({
|
|
name: opts.name,
|
|
url: opts.url,
|
|
description: opts.description,
|
|
logo: opts.logo ?? '',
|
|
positions,
|
|
status: 'published',
|
|
});
|
|
if (ctx.json) printJson(created);
|
|
else process.stdout.write(`✓ Plugin registered: ${(created as { id?: string }).id}\n`);
|
|
})
|
|
);
|
|
|
|
const config = cmd.command('config').description('Manage per-table plugin enablement');
|
|
config
|
|
.command('list')
|
|
.description('List plugin configs for the base')
|
|
.action(
|
|
run(async (ctx) => {
|
|
const list = await ctx.client.listPluginConfigs(ctx.baseId!);
|
|
if (ctx.json) printJson(list);
|
|
else {
|
|
printTable(
|
|
[
|
|
{ key: 'pluginId', header: 'PLUGIN' },
|
|
{ key: 'tableId', header: 'TABLE' },
|
|
{ key: 'enabled', header: 'ON' },
|
|
{ key: 'runtimeForm', header: 'FORM' },
|
|
{ key: 'writeEnabled', header: 'WRITE' },
|
|
],
|
|
(list as Array<Record<string, unknown>>).map((r) => ({
|
|
pluginId: r.pluginId,
|
|
tableId: r.tableId || '-',
|
|
enabled: r.enabled,
|
|
runtimeForm: r.runtimeForm,
|
|
writeEnabled: r.writeEnabled,
|
|
}))
|
|
);
|
|
}
|
|
})
|
|
);
|
|
|
|
config
|
|
.command('enable')
|
|
.description('Enable a plugin for a table (or base-wide if no --table)')
|
|
.requiredOption('--plugin <id>', 'Plugin ID')
|
|
.option('--table <id>', 'Table ID (omit for base-wide)')
|
|
.option('--write', 'Allow write-back')
|
|
.action(
|
|
run(async (ctx, _args, o) => {
|
|
const opts = o as { plugin: string; table?: string; write?: boolean };
|
|
const result = await ctx.client.upsertPluginConfig(ctx.baseId!, opts.plugin, {
|
|
enabled: true,
|
|
runtimeForm: 'panel',
|
|
writeEnabled: !!opts.write,
|
|
tableIds: opts.table ? [opts.table] : [],
|
|
});
|
|
if (ctx.json) printJson(result);
|
|
else
|
|
process.stdout.write(
|
|
`✓ Plugin ${opts.plugin} enabled${opts.table ? ` for table ${opts.table}` : ' base-wide'}\n`
|
|
);
|
|
})
|
|
);
|
|
|
|
config
|
|
.command('disable')
|
|
.description('Disable a plugin for a table (or base-wide if no --table)')
|
|
.requiredOption('--plugin <id>', 'Plugin ID')
|
|
.option('--table <id>', 'Table ID')
|
|
.action(
|
|
run(async (ctx, _args, o) => {
|
|
const opts = o as { plugin: string; table?: string };
|
|
await ctx.client.upsertPluginConfig(ctx.baseId!, opts.plugin, {
|
|
enabled: false,
|
|
runtimeForm: 'panel',
|
|
writeEnabled: false,
|
|
tableIds: opts.table ? [opts.table] : [],
|
|
});
|
|
process.stdout.write(
|
|
`✓ Plugin ${opts.plugin} disabled${opts.table ? ` for table ${opts.table}` : ' base-wide'}\n`
|
|
);
|
|
})
|
|
);
|
|
|
|
// Wire feature subcommands
|
|
addDevCommands(cmd);
|
|
addInstallCommands(cmd);
|
|
|
|
return cmd;
|
|
}
|