mirror of
https://github.com/allexcd/obsidian-mcp.git
synced 2026-07-22 17:00:24 +00:00
274 lines
8.7 KiB
JavaScript
Executable file
274 lines
8.7 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const readline = require('readline');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
const LOCK_PATH = path.join(ROOT, 'package-lock.json');
|
|
const ROOT_MANIFEST_PATH = path.join(ROOT, 'manifest.json');
|
|
const PLUGIN_MANIFEST_PATH = path.join(ROOT, 'packages', 'plugin', 'manifest.json');
|
|
const VERSIONS_PATH = path.join(ROOT, 'versions.json');
|
|
const WORKSPACE_PACKAGE_PATHS = [
|
|
path.join(ROOT, 'packages', 'shared', 'package.json'),
|
|
path.join(ROOT, 'packages', 'mcp-server', 'package.json'),
|
|
path.join(ROOT, 'packages', 'plugin', 'package.json')
|
|
];
|
|
const BUMP_TYPES = new Set(['patch', 'minor', 'major']);
|
|
|
|
function run(command, args = [], opts = {}) {
|
|
return execFileSync(command, args, {
|
|
cwd: ROOT,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
...opts,
|
|
}).trim();
|
|
}
|
|
|
|
function runInherit(command, args = []) {
|
|
execFileSync(command, args, { cwd: ROOT, stdio: 'inherit' });
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`
|
|
obsidian-mcp release
|
|
|
|
Usage:
|
|
npm run release
|
|
|
|
Steps:
|
|
1. Require a clean main branch
|
|
2. Fetch tags and choose patch/minor/major
|
|
3. Create release/X.Y.Z (no v prefix; matches Obsidian tag convention)
|
|
4. Bump package.json, package-lock.json, manifest.json,
|
|
packages/plugin/manifest.json, and append to versions.json
|
|
5. Run npm run build to confirm the bundle still produces a clean zip
|
|
6. Commit, push, and open a PR
|
|
|
|
After the PR is merged, GitHub Actions tags X.Y.Z, creates a GitHub Release,
|
|
and uploads main.js, manifest.json, styles.css, and the plugin zip.
|
|
`);
|
|
}
|
|
|
|
function fail(message, detail = '') {
|
|
console.error(`\n Error: ${message}\n`);
|
|
if (detail) {
|
|
console.error(detail);
|
|
console.error('');
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
function checkDependencies() {
|
|
try {
|
|
run('gh', ['--version']);
|
|
} catch {
|
|
fail('gh CLI is not installed or not in PATH.', ' Install it from https://cli.github.com and run "gh auth login" first.');
|
|
}
|
|
|
|
try {
|
|
run('gh', ['auth', 'status']);
|
|
} catch {
|
|
fail('gh CLI is not authenticated. Run "gh auth login" first.');
|
|
}
|
|
}
|
|
|
|
function assertSemver(version, label) {
|
|
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
fail(`${label} must be a plain semver version like 1.2.3. Found "${version}".`);
|
|
}
|
|
}
|
|
|
|
function bumpVersion(current, type) {
|
|
assertSemver(current, 'Current version');
|
|
const [major, minor, patch] = current.split('.').map(Number);
|
|
|
|
if (type === 'major') {
|
|
return `${major + 1}.0.0`;
|
|
}
|
|
if (type === 'minor') {
|
|
return `${major}.${minor + 1}.0`;
|
|
}
|
|
return `${major}.${minor}.${patch + 1}`;
|
|
}
|
|
|
|
function latestVersionTag() {
|
|
const tags = run('git', ['tag', '--sort=-v:refname'])
|
|
.split('\n')
|
|
.map((tag) => tag.trim())
|
|
.filter(Boolean);
|
|
|
|
return tags.find((tag) => /^\d+\.\d+\.\d+$/.test(tag)) || null;
|
|
}
|
|
|
|
function prompt(question) {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
rl.close();
|
|
resolve(answer.trim());
|
|
}));
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
}
|
|
|
|
function writeJson(filePath, data) {
|
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
}
|
|
|
|
async function main() {
|
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
const status = run('git', ['status', '--porcelain']);
|
|
if (status) {
|
|
fail('Working tree has uncommitted changes. Commit or stash them first.', status);
|
|
}
|
|
|
|
const currentBranch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
if (currentBranch !== 'main') {
|
|
fail(`Must be on main branch to create a release. Currently on "${currentBranch}".`);
|
|
}
|
|
|
|
checkDependencies();
|
|
|
|
run('git', ['fetch', '--tags']);
|
|
|
|
const pkg = readJson(PKG_PATH);
|
|
const rootManifest = readJson(ROOT_MANIFEST_PATH);
|
|
const pluginManifest = readJson(PLUGIN_MANIFEST_PATH);
|
|
assertSemver(pkg.version, 'package.json version');
|
|
assertSemver(rootManifest.version, 'manifest.json version');
|
|
assertSemver(pluginManifest.version, 'packages/plugin/manifest.json version');
|
|
|
|
if (rootManifest.version !== pkg.version || pluginManifest.version !== pkg.version) {
|
|
fail(
|
|
'Versions are out of sync. Align package.json, manifest.json, and packages/plugin/manifest.json before releasing.',
|
|
` package.json: ${pkg.version}\n manifest.json: ${rootManifest.version}\n packages/plugin/manifest.json: ${pluginManifest.version}`
|
|
);
|
|
}
|
|
|
|
let current = pkg.version;
|
|
const latestTag = latestVersionTag();
|
|
if (latestTag) {
|
|
if (latestTag !== pkg.version) {
|
|
console.log(`\n package.json (${pkg.version}) is behind latest tag (${latestTag}); using tag as base.`);
|
|
}
|
|
current = latestTag;
|
|
}
|
|
|
|
console.log(`\n Current version: ${current}`);
|
|
console.log(' Bump type: patch | minor | major\n');
|
|
|
|
const input = await prompt(' Bump type (patch): ');
|
|
const bumpType = input || 'patch';
|
|
if (!BUMP_TYPES.has(bumpType)) {
|
|
fail(`Invalid bump type "${bumpType}". Must be patch, minor, or major.`);
|
|
}
|
|
|
|
const next = bumpVersion(current, bumpType);
|
|
const tag = next; // No v prefix — Obsidian convention
|
|
const branch = `release/${tag}`;
|
|
const minAppVersion = rootManifest.minAppVersion;
|
|
|
|
console.log(`\n ${current} -> ${next}`);
|
|
console.log(` Tag: ${tag} (no v prefix, Obsidian convention)`);
|
|
console.log(` minAppVersion: ${minAppVersion}`);
|
|
console.log(` Branch: ${branch}\n`);
|
|
|
|
run('git', ['checkout', '-b', branch]);
|
|
|
|
pkg.version = next;
|
|
writeJson(PKG_PATH, pkg);
|
|
|
|
rootManifest.version = next;
|
|
writeJson(ROOT_MANIFEST_PATH, rootManifest);
|
|
|
|
pluginManifest.version = next;
|
|
writeJson(PLUGIN_MANIFEST_PATH, pluginManifest);
|
|
|
|
for (const workspacePackagePath of WORKSPACE_PACKAGE_PATHS) {
|
|
const workspacePackage = readJson(workspacePackagePath);
|
|
workspacePackage.version = next;
|
|
if (workspacePackage.dependencies && workspacePackage.dependencies['@obsidian-mcp/shared']) {
|
|
workspacePackage.dependencies['@obsidian-mcp/shared'] = next;
|
|
}
|
|
writeJson(workspacePackagePath, workspacePackage);
|
|
}
|
|
|
|
const versions = fs.existsSync(VERSIONS_PATH) ? readJson(VERSIONS_PATH) : {};
|
|
versions[next] = minAppVersion;
|
|
writeJson(VERSIONS_PATH, versions);
|
|
|
|
if (fs.existsSync(LOCK_PATH)) {
|
|
run('npm', ['install', '--package-lock-only', '--ignore-scripts']);
|
|
console.log(` OK package.json + package-lock.json + manifests + workspace packages + versions.json -> ${next}`);
|
|
} else {
|
|
console.log(` OK package.json + manifests + workspace packages + versions.json -> ${next}`);
|
|
}
|
|
|
|
console.log('\n Running npm run build to verify the bundle is clean...');
|
|
try {
|
|
runInherit('npm', ['run', 'build']);
|
|
} catch {
|
|
fail('Build failed. Fix build errors before releasing.');
|
|
}
|
|
|
|
// Build leaves files under build/ — they're gitignored, but verify just in case.
|
|
const dirty = run('git', ['status', '--porcelain']);
|
|
if (dirty.split('\n').some((line) => line && !line.match(/(package\.json|package-lock\.json|manifest\.json|versions\.json)$/))) {
|
|
fail(
|
|
'Unexpected files would be committed (anything beyond version files).',
|
|
dirty
|
|
);
|
|
}
|
|
|
|
const filesToCommit = [
|
|
'package.json',
|
|
'manifest.json',
|
|
'packages/plugin/manifest.json',
|
|
'packages/shared/package.json',
|
|
'packages/mcp-server/package.json',
|
|
'packages/plugin/package.json',
|
|
'versions.json'
|
|
];
|
|
if (fs.existsSync(LOCK_PATH)) {
|
|
filesToCommit.push('package-lock.json');
|
|
}
|
|
|
|
run('git', ['add', ...filesToCommit]);
|
|
run('git', ['commit', '-m', `chore: release ${tag}`]);
|
|
console.log(` OK commit: chore: release ${tag}`);
|
|
|
|
run('git', ['push', '-u', 'origin', branch]);
|
|
console.log(` OK pushed branch: ${branch}`);
|
|
|
|
const prBody = [
|
|
`Bump version to ${next}.`,
|
|
'',
|
|
`Once merged, CI will tag \`${tag}\` (no v prefix, Obsidian convention),`,
|
|
`create a GitHub Release with auto-generated notes, and upload the plugin`,
|
|
`assets (\`main.js\`, \`manifest.json\`, \`styles.css\`, and the zip archive).`
|
|
].join('\n');
|
|
const prBodyPath = path.join(os.tmpdir(), `obsidian-mcp-release-${next}.md`);
|
|
fs.writeFileSync(prBodyPath, prBody, 'utf8');
|
|
try {
|
|
run('gh', ['pr', 'create', '--title', `chore: release ${tag}`, '--body-file', prBodyPath, '--base', 'main']);
|
|
} finally {
|
|
fs.unlinkSync(prBodyPath);
|
|
}
|
|
console.log(' OK PR opened against main');
|
|
|
|
console.log('\n Done. Merge the PR to trigger tagging and release asset upload.\n');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
fail(`Release failed: ${err.message}`);
|
|
});
|