#!/usr/bin/env node const fs = require('fs'); const { OPERATION_HELP } = require('./lib/maintainer-cli-operation-help.js'); const { spawnSyncWithCommandResolution } = require('./lib/cross-platform-command.js'); function printUsage() { const operationDetails = Object.entries(OPERATION_HELP) .map(([operationId, details]) => { const optionalLine = details.optional.length > 0 ? `\n optional: ${details.optional.join(', ')}` : ''; const exampleLine = details.exampleInput ? `\n example input: ${details.exampleInput}` : ''; const additionalExamplesLine = Array.isArray(details.additionalExamples) && details.additionalExamples.length > 0 ? `\n additional examples:\n${details.additionalExamples.map((example) => ` - ${example}`).join('\n')}` : ''; return ` ${operationId}\n ${details.summary}\n required: ${details.required.join(', ')}${optionalLine}${exampleLine}${additionalExamplesLine}`; }) .join('\n\n'); console.log(`Notemd maintainer CLI helper Help: npm run cli:help node scripts/invoke-maintainer-cli-operation.js --help Invoke: npm run cli:invoke -- --vault --operation [--input-file | --input-json ''] [--pretty] node scripts/invoke-maintainer-cli-operation.js \\ --vault \\ --operation \\ [--input-file | --input-json ''] \\ [--plugin-id ] \\ [--pretty] Operations: ${operationDetails} Minimal examples: npm run cli:invoke -- --vault docs --operation content.split-note-by-chapters --input-json '{"sourcePath":"index.zh-CN.md"}' --pretty npm run cli:invoke -- --vault docs --operation local-knowledge.inspect --input-json '{"taskScope":"diagramGeneration","sourcePath":"index.zh-CN.md","knowledgePaths":["maintainer","superpowers"]}' --pretty npm run cli:invoke -- --vault docs --operation provider.profile.export-redacted --pretty Notes: - Host selection prefers obsidian-cli native eval when that wrapper exists, then falls back to the official obsidian eval command. - Prefer --input-file for non-trivial payloads. - Paths inside --input-json are vault-relative. For --vault docs, use "index.zh-CN.md" and "maintainer", not "docs/index.zh-CN.md" or "docs/maintainer". - Maintainer bridge only; not a public CLI surface. `); } function parseArgs(argv) { const args = { pluginId: 'notemd', pretty: false }; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; switch (token) { case '--vault': args.vault = argv[++i]; break; case '--operation': args.operationId = argv[++i]; break; case '--plugin-id': args.pluginId = argv[++i]; break; case '--input-json': args.inputJson = argv[++i]; break; case '--input-file': args.inputFile = argv[++i]; break; case '--pretty': args.pretty = true; break; case '--help': case '-h': args.help = true; break; default: throw new Error(`Unknown argument: ${token}`); } } return args; } function loadInput(args) { if (args.inputJson && args.inputFile) { throw new Error('Use either --input-json or --input-file, not both.'); } if (args.inputFile) { return JSON.parse(fs.readFileSync(args.inputFile, 'utf8')); } if (args.inputJson) { return JSON.parse(args.inputJson); } return {}; } function buildEvalCode(pluginId, request) { const requestLiteral = JSON.stringify(JSON.stringify(request)); const pluginIdLiteral = JSON.stringify(pluginId); return `(async () => { const plugin = app.plugins.getPlugin(${pluginIdLiteral}); if (!plugin) { throw new Error('Plugin not loaded: ' + ${pluginIdLiteral}); } if (typeof plugin.invokeMaintainerCliOperation !== 'function') { throw new Error('Plugin does not expose invokeMaintainerCliOperation'); } const request = JSON.parse(${requestLiteral}); const result = await plugin.invokeMaintainerCliOperation(request); return JSON.stringify(result); })()`.replace(/\s+/g, ' ').trim(); } function extractEvalResult(stdout) { const trimmedStdout = stdout.trim(); if (trimmedStdout.startsWith('Error:')) { throw new Error(trimmedStdout); } const lines = stdout .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); const evalLine = [...lines].reverse().find((line) => line.startsWith('=> ')); if (!evalLine) { throw new Error(`Could not parse eval output:\n${stdout}`); } return evalLine.slice(3); } function isCommandUnavailable(result) { const code = String(result.error?.code || '').toUpperCase(); return result.status === null && (code === 'ENOENT' || code === 'EINVAL'); } function invokeObsidianEval(vault, code) { const options = { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }; const wrapperResult = spawnSyncWithCommandResolution( 'obsidian-cli', ['native', `vault=${vault}`, 'eval', `code=${code}`], options ); if (!isCommandUnavailable(wrapperResult)) { return { commandLabel: 'obsidian-cli native eval', result: wrapperResult }; } return { commandLabel: 'obsidian eval', result: spawnSyncWithCommandResolution( 'obsidian', ['eval', `vault=${vault}`, `code=${code}`], options ) }; } function main() { try { const args = parseArgs(process.argv.slice(2)); if (args.help) { printUsage(); process.exit(0); } if (!args.vault) { throw new Error('Missing required --vault.'); } if (!args.operationId) { throw new Error('Missing required --operation.'); } if (!Object.prototype.hasOwnProperty.call(OPERATION_HELP, args.operationId)) { throw new Error(`Unsupported operation: ${args.operationId}`); } const input = loadInput(args); const request = { operationId: args.operationId, input }; const code = buildEvalCode(args.pluginId, request); const invocation = invokeObsidianEval(args.vault, code); const child = invocation.result; if (child.status !== 0) { process.stderr.write(child.stderr || child.stdout || `${invocation.commandLabel} failed\n`); process.exit(child.status || 1); } const rawResult = extractEvalResult(child.stdout); const parsed = JSON.parse(rawResult); process.stdout.write(`${JSON.stringify(parsed, null, args.pretty ? 2 : 0)}\n`); } catch (error) { const message = error instanceof Error ? error.message : String(error); process.stderr.write(`${message}\n`); process.exit(1); } } main();