mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-20 03:57:30 +08:00
feat(mcp): add torrent filter workflow and moviepilot cli skill
This commit is contained in:
79
skills/moviepilot-cli/SKILL.md
Normal file
79
skills/moviepilot-cli/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: moviepilot-cli
|
||||
description: Use this skill when the user wants to manage a home media ecosystem via MoviePilot. Covers searching movies/TV shows/anime, managing subscriptions, controlling downloads (torrent search, quality filtering), monitoring download progress, and organizing media libraries. Trigger when user mentions movie/show titles, asks about subscriptions, downloads, library organization, or references MoviePilot directly.
|
||||
---
|
||||
|
||||
# MoviePilot Media Management Skill
|
||||
|
||||
## Overview
|
||||
|
||||
This skill interacts with the MoviePilot backend via the Node.js command-line script `scripts/mp-cli.js`. It supports four core capabilities: media search and recognition, subscription management, download control, and media library organization.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
Usage: mp-cli.js [-h HOST] [-k KEY] [COMMAND] [ARGS...]
|
||||
|
||||
Options:
|
||||
-h HOST backend host
|
||||
-k KEY API key
|
||||
|
||||
Commands:
|
||||
(no command) save config when -h and -k are provided
|
||||
list list all commands
|
||||
show <command> show command details and usage example
|
||||
<command> [k=v...] run a command
|
||||
```
|
||||
|
||||
## Discovering Available Tools
|
||||
|
||||
Before performing any task, use these two commands to understand what the current environment supports.
|
||||
|
||||
**List all available commands:**
|
||||
|
||||
```bash
|
||||
node scripts/mp-cli.js list
|
||||
```
|
||||
|
||||
**Inspect a command's parameters:**
|
||||
|
||||
```bash
|
||||
node scripts/mp-cli.js show <command>
|
||||
```
|
||||
|
||||
`show` displays a command's name, its parameters, and a usage example. For each parameter, it shows the name, type, required/optional status, and description. **Always run `show` before calling any command** — never guess parameter names or formats.
|
||||
|
||||
## Standard Workflow
|
||||
|
||||
Follow this sequence for any media task:
|
||||
|
||||
```
|
||||
1. list → confirm which commands are available
|
||||
2. show <command> → confirm parameter format before calling
|
||||
3. Search / recognize → resolve exact metadata (TMDB ID, season, episode)
|
||||
4. Check library / subs → avoid duplicate downloads or subscriptions
|
||||
5. Execute action → downloads require explicit user confirmation first
|
||||
6. Confirm final state → report the outcome to the user
|
||||
```
|
||||
|
||||
## Tool Calling Strategy
|
||||
|
||||
**Fallback search**: If a media search returns no results, try in order: fuzzy recognition → web search → ask the user for more information.
|
||||
|
||||
**Disambiguation**: If search results are ambiguous, call the detail-query command to obtain precise metadata before proceeding.
|
||||
|
||||
## Download Safety Rules
|
||||
|
||||
Before executing any download command, you **must**:
|
||||
|
||||
1. Search for and retrieve a list of available torrent resources.
|
||||
2. Present torrent details to the user (size, seeders, quality, release group).
|
||||
3. **Wait for explicit user confirmation** before initiating the download.
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
| --------------------- | --------------------------------------------------------------------------- |
|
||||
| No search results | Try fuzzy recognition → web search → ask the user |
|
||||
| Download failure | Check downloader status; advise user to verify disk space |
|
||||
| Missing configuration | Prompt user to run `node scripts/mp-cli.js -h <HOST> -k <KEY>` to configure |
|
||||
593
skills/moviepilot-cli/scripts/mp-cli.js
Executable file
593
skills/moviepilot-cli/scripts/mp-cli.js
Executable file
@@ -0,0 +1,593 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const SCRIPT_NAME = process.env.MP_SCRIPT_NAME || path.basename(process.argv[1] || 'mp-cli.js');
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.config', 'moviepilot_cli');
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
|
||||
|
||||
let commandsJson = [];
|
||||
let commandsLoaded = false;
|
||||
|
||||
let optHost = '';
|
||||
let optKey = '';
|
||||
|
||||
const envHost = process.env.MP_HOST || '';
|
||||
const envKey = process.env.MP_API_KEY || '';
|
||||
|
||||
let mpHost = '';
|
||||
let mpApiKey = '';
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function spacePad(text = '', targetCol = 0) {
|
||||
const spaces = text.length < targetCol ? targetCol - text.length + 2 : 2;
|
||||
return ' '.repeat(spaces);
|
||||
}
|
||||
|
||||
function printBox(title, lines) {
|
||||
const rightPadding = 0;
|
||||
const contentWidth =
|
||||
lines.reduce((max, line) => Math.max(max, line.length), title.length) + rightPadding;
|
||||
const innerWidth = contentWidth + 2;
|
||||
const topLabel = `─ ${title}`;
|
||||
|
||||
console.error(`┌${topLabel}${'─'.repeat(Math.max(innerWidth - topLabel.length, 0))}┐`);
|
||||
for (const line of lines) {
|
||||
console.error(`│ ${line}${' '.repeat(contentWidth - line.length)} │`);
|
||||
}
|
||||
console.error(`└${'─'.repeat(innerWidth)}┘`);
|
||||
}
|
||||
|
||||
function readConfig() {
|
||||
let cfgHost = '';
|
||||
let cfgKey = '';
|
||||
|
||||
if (!fs.existsSync(CONFIG_FILE)) {
|
||||
return { cfgHost, cfgKey };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
if (!line.trim() || /^\s*#/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = line.indexOf('=');
|
||||
if (index === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, index).replace(/\s+/g, '');
|
||||
const value = line.slice(index + 1);
|
||||
|
||||
if (key === 'MP_HOST') {
|
||||
cfgHost = value;
|
||||
} else if (key === 'MP_API_KEY') {
|
||||
cfgKey = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { cfgHost, cfgKey };
|
||||
}
|
||||
|
||||
function saveConfig(host, key) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(CONFIG_FILE, `MP_HOST=${host}\nMP_API_KEY=${key}\n`, 'utf8');
|
||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
const { cfgHost: initialHost, cfgKey: initialKey } = readConfig();
|
||||
let cfgHost = initialHost;
|
||||
let cfgKey = initialKey;
|
||||
|
||||
if (optHost || optKey) {
|
||||
const nextHost = optHost || cfgHost;
|
||||
const nextKey = optKey || cfgKey;
|
||||
saveConfig(nextHost, nextKey);
|
||||
cfgHost = nextHost;
|
||||
cfgKey = nextKey;
|
||||
}
|
||||
|
||||
mpHost = optHost || mpHost || envHost || cfgHost;
|
||||
mpApiKey = optKey || mpApiKey || envKey || cfgKey;
|
||||
}
|
||||
|
||||
function normalizeType(schema = {}) {
|
||||
if (schema.type) {
|
||||
return schema.type;
|
||||
}
|
||||
if (Array.isArray(schema.anyOf)) {
|
||||
const candidate = schema.anyOf.find((item) => item && item.type && item.type !== 'null');
|
||||
return candidate?.type || 'string';
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
|
||||
function normalizeItemType(schema = {}) {
|
||||
const items = schema.items;
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
if (items.type) {
|
||||
return items.type;
|
||||
}
|
||||
if (Array.isArray(items.anyOf)) {
|
||||
const candidate = items.anyOf.find((item) => item && item.type && item.type !== 'null');
|
||||
return candidate?.type || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function request(method, targetUrl, headers = {}, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(targetUrl);
|
||||
} catch (error) {
|
||||
reject(new Error(`Invalid URL: ${targetUrl}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
const req = transport.request(
|
||||
{
|
||||
method,
|
||||
hostname: url.hostname,
|
||||
port: url.port || undefined,
|
||||
path: `${url.pathname}${url.search}`,
|
||||
headers,
|
||||
},
|
||||
(res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode ? String(res.statusCode) : '',
|
||||
body: Buffer.concat(chunks).toString('utf8'),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (body !== undefined) {
|
||||
req.write(body);
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCommandsJson() {
|
||||
if (commandsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { statusCode, body } = await request('GET', `${mpHost}/api/v1/mcp/tools`, {
|
||||
'X-API-KEY': mpApiKey,
|
||||
});
|
||||
|
||||
if (statusCode !== '200') {
|
||||
console.error(`Error: failed to load command definitions (HTTP ${statusCode || 'unknown'})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(body);
|
||||
} catch {
|
||||
fail('Error: backend returned invalid JSON for command definitions');
|
||||
}
|
||||
|
||||
commandsJson = Array.isArray(response)
|
||||
? response
|
||||
.map((tool) => {
|
||||
const properties = tool?.inputSchema?.properties || {};
|
||||
const required = Array.isArray(tool?.inputSchema?.required) ? tool.inputSchema.required : [];
|
||||
const fields = Object.entries(properties)
|
||||
.filter(([fieldName]) => fieldName !== 'explanation')
|
||||
.map(([fieldName, schema]) => ({
|
||||
name: fieldName,
|
||||
type: normalizeType(schema),
|
||||
description: schema?.description || '',
|
||||
required: required.includes(fieldName),
|
||||
item_type: normalizeItemType(schema),
|
||||
}));
|
||||
|
||||
return {
|
||||
name: tool?.name,
|
||||
description: tool?.description || '',
|
||||
fields,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
commandsLoaded = true;
|
||||
}
|
||||
|
||||
function ensureConfig() {
|
||||
loadConfig();
|
||||
let ok = true;
|
||||
|
||||
if (!mpHost) {
|
||||
console.error('Error: backend host is not configured.');
|
||||
console.error(' Use: -h HOST to set it');
|
||||
console.error(' Or set environment variable: MP_HOST=http://localhost:3001');
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!mpApiKey) {
|
||||
console.error('Error: API key is not configured.');
|
||||
console.error(' Use: -k KEY to set it');
|
||||
console.error(' Or set environment variable: MP_API_KEY=your_key');
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function printValue(value) {
|
||||
if (typeof value === 'string') {
|
||||
process.stdout.write(`${value}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function cmdList() {
|
||||
await loadCommandsJson();
|
||||
for (const command of commandsJson) {
|
||||
process.stdout.write(`- ${command.name}${spacePad(command.name)}${command.description}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdShow(commandName) {
|
||||
await loadCommandsJson();
|
||||
|
||||
if (!commandName) {
|
||||
fail(`Usage: ${SCRIPT_NAME} show <command>`);
|
||||
}
|
||||
|
||||
const command = commandsJson.find((item) => item.name === commandName);
|
||||
if (!command) {
|
||||
console.error(`Error: command '${commandName}' not found`);
|
||||
console.error(`Run '${SCRIPT_NAME} list' to see available commands`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const commandLabel = 'Command:';
|
||||
const paramsLabel = 'Parameters:';
|
||||
const usageLabel = 'Usage example:';
|
||||
const detailLabelWidth = Math.max(commandLabel.length, paramsLabel.length, usageLabel.length);
|
||||
|
||||
process.stdout.write(`${commandLabel} ${command.name}\n\n`);
|
||||
|
||||
if (command.fields.length === 0) {
|
||||
process.stdout.write(`${paramsLabel}${spacePad(paramsLabel, detailLabelWidth)}(none)\n`);
|
||||
} else {
|
||||
const fieldLines = command.fields.map((field) => [
|
||||
field.name,
|
||||
field.type,
|
||||
field.required ? '[required]' : '[optional]',
|
||||
field.description,
|
||||
]);
|
||||
|
||||
const nameWidth = Math.max(...fieldLines.map(([name]) => name.length), 0);
|
||||
const typeWidth = Math.max(...fieldLines.map(([, type]) => type.length), 0);
|
||||
const reqWidth = Math.max(...fieldLines.map(([, , required]) => required.length), 0);
|
||||
|
||||
process.stdout.write(`${paramsLabel}\n`);
|
||||
for (const [fieldName, fieldType, fieldRequired, fieldDesc] of fieldLines) {
|
||||
process.stdout.write(
|
||||
` ${fieldName}${spacePad(fieldName, nameWidth)}${fieldType}${spacePad(fieldType, typeWidth)}${fieldRequired}${spacePad(fieldRequired, reqWidth)}${fieldDesc}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const usageLine = `${SCRIPT_NAME} ${command.name}`;
|
||||
const reqPart = command.fields.filter((field) => field.required).map((field) => ` ${field.name}=<value>`).join('');
|
||||
const optPart = command.fields
|
||||
.filter((field) => !field.required)
|
||||
.map((field) => ` [${field.name}=<value>]`)
|
||||
.join('');
|
||||
|
||||
process.stdout.write(
|
||||
`\n${usageLabel}${spacePad(usageLabel, detailLabelWidth)}${usageLine}${reqPart}${optPart}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function parseBoolean(value) {
|
||||
return value === 'true' || value === '1' || value === 'yes';
|
||||
}
|
||||
|
||||
function parseNumber(value, key) {
|
||||
if (value === '') {
|
||||
fail(`Error: invalid numeric value for '${key}'`);
|
||||
}
|
||||
|
||||
const result = Number(value);
|
||||
if (Number.isNaN(result)) {
|
||||
fail(`Error: invalid numeric value for '${key}': '${value}'`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseScalarValue(value, key, type = 'string') {
|
||||
if (type === 'integer' || type === 'number') {
|
||||
return parseNumber(value, key);
|
||||
}
|
||||
|
||||
if (type === 'boolean') {
|
||||
return parseBoolean(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseArrayValue(value, key, itemType = 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('[')) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch {
|
||||
fail(`Error: invalid array value for '${key}': '${value}'`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
fail(`Error: invalid array value for '${key}': '${value}'`);
|
||||
}
|
||||
|
||||
return parsed.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return parseScalarValue(item.trim(), key, itemType);
|
||||
}
|
||||
if (itemType === 'integer' || itemType === 'number') {
|
||||
if (typeof item !== 'number') {
|
||||
fail(`Error: invalid numeric value for '${key}': '${item}'`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
if (itemType === 'boolean') {
|
||||
if (typeof item !== 'boolean') {
|
||||
fail(`Error: invalid boolean value for '${key}': '${item}'`);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
return trimmed
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.map((item) => parseScalarValue(item, key, itemType));
|
||||
}
|
||||
|
||||
function buildArguments(command, pairs) {
|
||||
const args = { explanation: 'CLI invocation' };
|
||||
|
||||
for (const kv of pairs) {
|
||||
if (!kv.includes('=')) {
|
||||
fail(`Error: argument must be in key=value format, got: '${kv}'`);
|
||||
}
|
||||
|
||||
const index = kv.indexOf('=');
|
||||
const key = kv.slice(0, index);
|
||||
const value = kv.slice(index + 1);
|
||||
const field = command.fields.find((item) => item.name === key);
|
||||
const fieldType = field?.type || 'string';
|
||||
const itemType = field?.item_type || 'string';
|
||||
|
||||
if (fieldType === 'array') {
|
||||
args[key] = parseArrayValue(value, key, itemType);
|
||||
continue;
|
||||
}
|
||||
|
||||
args[key] = parseScalarValue(value, key, fieldType);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
async function cmdRun(commandName, pairs) {
|
||||
await loadCommandsJson();
|
||||
|
||||
if (!commandName) {
|
||||
fail(`Usage: ${SCRIPT_NAME} <command> [key=value ...]`);
|
||||
}
|
||||
|
||||
const command = commandsJson.find((item) => item.name === commandName);
|
||||
if (!command) {
|
||||
console.error(`Error: command '${commandName}' not found`);
|
||||
console.error(`Run '${SCRIPT_NAME} list' to see available commands`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const requestBody = JSON.stringify({
|
||||
tool_name: commandName,
|
||||
arguments: buildArguments(command, pairs),
|
||||
});
|
||||
|
||||
const { statusCode, body } = await request(
|
||||
'POST',
|
||||
`${mpHost}/api/v1/mcp/tools/call`,
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestBody),
|
||||
'X-API-KEY': mpApiKey,
|
||||
},
|
||||
requestBody
|
||||
);
|
||||
|
||||
if (statusCode && statusCode !== '200' && statusCode !== '201') {
|
||||
console.error(`Warning: HTTP status ${statusCode}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, 'result')) {
|
||||
if (typeof parsed.result === 'string') {
|
||||
try {
|
||||
printValue(JSON.parse(parsed.result));
|
||||
} catch {
|
||||
printValue(parsed.result);
|
||||
}
|
||||
} else {
|
||||
printValue(parsed.result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
printValue(parsed);
|
||||
} catch {
|
||||
process.stdout.write(`${body}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
const { cfgHost, cfgKey } = readConfig();
|
||||
let effectiveHost = mpHost || envHost || cfgHost;
|
||||
let effectiveKey = mpApiKey || envKey || cfgKey;
|
||||
|
||||
if (optHost) {
|
||||
effectiveHost = optHost;
|
||||
}
|
||||
if (optKey) {
|
||||
effectiveKey = optKey;
|
||||
}
|
||||
|
||||
if (!effectiveHost || !effectiveKey) {
|
||||
const warningLines = [];
|
||||
if (!effectiveHost) {
|
||||
const opt = '-h HOST';
|
||||
const desc = 'set backend host';
|
||||
warningLines.push(`${opt}${spacePad(opt)}${desc}`);
|
||||
}
|
||||
if (!effectiveKey) {
|
||||
const opt = '-k KEY';
|
||||
const desc = 'set API key';
|
||||
warningLines.push(`${opt}${spacePad(opt)}${desc}`);
|
||||
}
|
||||
printBox('Warning: not configured', warningLines);
|
||||
console.error('');
|
||||
}
|
||||
|
||||
process.stdout.write(`Usage: ${SCRIPT_NAME} [-h HOST] [-k KEY] [COMMAND] [ARGS...]\n\n`);
|
||||
const optionWidth = Math.max('-h HOST'.length, '-k KEY'.length);
|
||||
process.stdout.write('Options:\n');
|
||||
process.stdout.write(` -h HOST${spacePad('-h HOST', optionWidth)}backend host\n`);
|
||||
process.stdout.write(` -k KEY${spacePad('-k KEY', optionWidth)}API key\n\n`);
|
||||
const commandWidth = Math.max(
|
||||
'(no command)'.length,
|
||||
'list'.length,
|
||||
'show <command>'.length,
|
||||
'<command> [k=v...]'.length
|
||||
);
|
||||
process.stdout.write('Commands:\n');
|
||||
process.stdout.write(
|
||||
` (no command)${spacePad('(no command)', commandWidth)}save config when -h and -k are provided\n`
|
||||
);
|
||||
process.stdout.write(` list${spacePad('list', commandWidth)}list all commands\n`);
|
||||
process.stdout.write(
|
||||
` show <command>${spacePad('show <command>', commandWidth)}show command details and usage example\n`
|
||||
);
|
||||
process.stdout.write(
|
||||
` <command> [k=v...]${spacePad('<command> [k=v...]', commandWidth)}run a command\n`
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = [];
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
|
||||
if (arg === '--help' || arg === '-?') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (arg === '-h') {
|
||||
index += 1;
|
||||
optHost = argv[index] || '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '-k') {
|
||||
index += 1;
|
||||
optKey = argv[index] || '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--') {
|
||||
args.push(...argv.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arg.startsWith('-')) {
|
||||
console.error(`Unknown option: ${arg}`);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
args.push(arg);
|
||||
}
|
||||
|
||||
if ((optHost && !optKey) || (!optHost && optKey)) {
|
||||
fail('Error: -h and -k must be provided together');
|
||||
}
|
||||
|
||||
const command = args[0] || '';
|
||||
|
||||
if (command === 'list') {
|
||||
ensureConfig();
|
||||
await cmdList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'show') {
|
||||
ensureConfig();
|
||||
await cmdShow(args[1] || '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
if (optHost || optKey) {
|
||||
loadConfig();
|
||||
process.stdout.write('Configuration saved.\n');
|
||||
return;
|
||||
}
|
||||
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
ensureConfig();
|
||||
await cmdRun(command, args.slice(1));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(`Error: ${error.message}`);
|
||||
});
|
||||
Reference in New Issue
Block a user