317 lines
10 KiB
JavaScript
317 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Astro Modular Update Script
|
|
*
|
|
* Downloads the latest release from GitHub, replaces framework files,
|
|
* and preserves user content and assets. After updating, open Obsidian
|
|
* and click "Apply all settings" in the Astro Modular Settings plugin
|
|
* to rewrite your config.ts with your saved settings.
|
|
*
|
|
* Usage: pnpm run update
|
|
*/
|
|
|
|
import { execSync } from 'child_process';
|
|
import { createWriteStream, existsSync, mkdirSync, cpSync, rmSync, readFileSync, readdirSync, statSync, renameSync, writeFileSync } from 'fs';
|
|
import { join, resolve, basename } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { get as httpsGet } from 'https';
|
|
import { pipeline } from 'stream/promises';
|
|
|
|
const ROOT = resolve(import.meta.dirname, '..');
|
|
const REPO = 'davidvkimball/astro-modular';
|
|
|
|
// Files and directories that belong to the USER and must be preserved
|
|
const USER_PATHS = [
|
|
'src/content', // All user content + .obsidian vault (plugin data.json files)
|
|
'public/profile.jpg',
|
|
'public/profile.png',
|
|
'public/profile.webp',
|
|
'public/favicon.png',
|
|
'public/favicon-dark.png',
|
|
'public/favicon-light.png',
|
|
'public/open-graph.png',
|
|
'.env',
|
|
'.env.local',
|
|
'.env.production',
|
|
];
|
|
|
|
// Directories that should never be touched
|
|
const SKIP_PATHS = [
|
|
'node_modules',
|
|
'.git',
|
|
'.astro',
|
|
'.netlify',
|
|
];
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
function log(msg) { console.log(` ${msg}`); }
|
|
function logStep(msg) { console.log(`\n> ${msg}`); }
|
|
function logError(msg) { console.error(`\n ERROR: ${msg}`); }
|
|
|
|
function getCurrentVersion() {
|
|
const versionFile = join(ROOT, 'VERSION');
|
|
if (existsSync(versionFile)) {
|
|
return readFileSync(versionFile, 'utf-8').trim();
|
|
}
|
|
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
|
return pkg.version;
|
|
}
|
|
|
|
async function fetchJSON(url) {
|
|
return new Promise((resolve, reject) => {
|
|
httpsGet(url, { headers: { 'User-Agent': 'astro-modular-updater' } }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
return fetchJSON(res.headers.location).then(resolve, reject);
|
|
}
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try { resolve(JSON.parse(data)); }
|
|
catch { reject(new Error(`Invalid JSON from ${url}`)); }
|
|
});
|
|
res.on('error', reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function downloadFile(url, dest) {
|
|
return new Promise((resolve, reject) => {
|
|
httpsGet(url, { headers: { 'User-Agent': 'astro-modular-updater' } }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
return downloadFile(res.headers.location, dest).then(resolve, reject);
|
|
}
|
|
if (res.statusCode !== 200) {
|
|
return reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
}
|
|
const ws = createWriteStream(dest);
|
|
pipeline(res, ws).then(resolve, reject);
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
function copyIfExists(src, dest) {
|
|
if (!existsSync(src)) return false;
|
|
const stat = statSync(src);
|
|
if (stat.isDirectory()) {
|
|
cpSync(src, dest, { recursive: true });
|
|
} else {
|
|
const dir = join(dest, '..');
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
cpSync(src, dest);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ── Main ────────────────────────────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
console.log('\nAstro Modular Updater');
|
|
console.log('====================');
|
|
|
|
// 1. Get current version
|
|
const currentVersion = getCurrentVersion();
|
|
log(`Current version: ${currentVersion}`);
|
|
|
|
// 2. Check latest release
|
|
logStep('Checking for updates...');
|
|
let release;
|
|
try {
|
|
release = await fetchJSON(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
} catch (err) {
|
|
logError(`Could not reach GitHub: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const latestVersion = release.tag_name.replace(/^v/, '');
|
|
log(`Latest version: ${latestVersion}`);
|
|
|
|
if (currentVersion === latestVersion) {
|
|
log('Already up to date!');
|
|
process.exit(0);
|
|
}
|
|
|
|
// 3. Download release archive
|
|
logStep(`Downloading v${latestVersion}...`);
|
|
const isWindows = process.platform === 'win32';
|
|
const archiveUrl = isWindows ? release.zipball_url : release.tarball_url;
|
|
const tempDir = join(tmpdir(), `astro-modular-update-${Date.now()}`);
|
|
mkdirSync(tempDir, { recursive: true });
|
|
const archivePath = join(tempDir, isWindows ? 'release.zip' : 'release.tar.gz');
|
|
|
|
try {
|
|
await downloadFile(archiveUrl, archivePath);
|
|
} catch (err) {
|
|
logError(`Download failed: ${err.message}`);
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
process.exit(1);
|
|
}
|
|
log(`Downloaded to temp directory.`);
|
|
|
|
// 4. Extract archive
|
|
logStep('Extracting...');
|
|
const extractDir = join(tempDir, 'extracted');
|
|
mkdirSync(extractDir, { recursive: true });
|
|
try {
|
|
if (isWindows) {
|
|
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force"`, { stdio: 'pipe' });
|
|
} else {
|
|
execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, { stdio: 'pipe' });
|
|
}
|
|
} catch (err) {
|
|
logError(`Extraction failed: ${err.message}`);
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
process.exit(1);
|
|
}
|
|
|
|
// GitHub tarballs extract into a directory like "owner-repo-hash/"
|
|
const extractedContents = readdirSync(extractDir);
|
|
if (extractedContents.length !== 1) {
|
|
logError('Unexpected archive structure.');
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
process.exit(1);
|
|
}
|
|
const sourceDir = join(extractDir, extractedContents[0]);
|
|
log('Extracted successfully.');
|
|
|
|
// 5. Back up user files
|
|
logStep('Backing up user content...');
|
|
const backupDir = join(tempDir, 'backup');
|
|
mkdirSync(backupDir, { recursive: true });
|
|
|
|
let backedUp = 0;
|
|
for (const userPath of USER_PATHS) {
|
|
const src = join(ROOT, userPath);
|
|
const dest = join(backupDir, userPath);
|
|
if (copyIfExists(src, dest)) {
|
|
log(` Backed up: ${userPath}`);
|
|
backedUp++;
|
|
}
|
|
}
|
|
|
|
// Also back up any user-added public/ files not in the new release
|
|
const publicDir = join(ROOT, 'public');
|
|
const newPublicDir = join(sourceDir, 'public');
|
|
if (existsSync(publicDir)) {
|
|
for (const file of readdirSync(publicDir)) {
|
|
const fullPath = join(publicDir, file);
|
|
if (statSync(fullPath).isFile()) {
|
|
const alreadyBacked = USER_PATHS.some(p => p === `public/${file}`);
|
|
if (!alreadyBacked) {
|
|
// Check if this file does NOT exist in the new release (user-added)
|
|
if (!existsSync(join(newPublicDir, file))) {
|
|
const dest = join(backupDir, 'public', file);
|
|
mkdirSync(join(backupDir, 'public'), { recursive: true });
|
|
cpSync(fullPath, dest);
|
|
log(` Backed up (user asset): public/${file}`);
|
|
backedUp++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log(`${backedUp} item(s) backed up.`);
|
|
|
|
// 6. Replace framework files
|
|
logStep('Updating framework files...');
|
|
|
|
// Get list of all paths in the new release (excluding user/skip paths)
|
|
function listPaths(dir, base = '') {
|
|
const results = [];
|
|
for (const entry of readdirSync(dir)) {
|
|
const rel = base ? `${base}/${entry}` : entry;
|
|
const full = join(dir, entry);
|
|
|
|
// Skip paths we should never touch
|
|
if (SKIP_PATHS.includes(rel)) continue;
|
|
// Skip user content paths (we'll restore from backup)
|
|
if (USER_PATHS.includes(rel)) continue;
|
|
|
|
if (statSync(full).isDirectory()) {
|
|
// If it's a user directory root, skip recursing
|
|
if (USER_PATHS.some(p => p.startsWith(rel + '/'))) {
|
|
// Only skip exact user-path prefixes like src/content
|
|
if (USER_PATHS.includes(rel)) continue;
|
|
results.push(...listPaths(full, rel));
|
|
} else {
|
|
results.push(...listPaths(full, rel));
|
|
}
|
|
} else {
|
|
results.push(rel);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
const newFiles = listPaths(sourceDir);
|
|
let updated = 0;
|
|
|
|
for (const relPath of newFiles) {
|
|
const src = join(sourceDir, relPath);
|
|
const dest = join(ROOT, relPath);
|
|
const destDir = join(dest, '..');
|
|
|
|
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
cpSync(src, dest);
|
|
updated++;
|
|
}
|
|
log(`${updated} framework file(s) updated.`);
|
|
|
|
// 7. Restore user files from backup
|
|
logStep('Restoring user content...');
|
|
let restored = 0;
|
|
|
|
for (const userPath of USER_PATHS) {
|
|
const src = join(backupDir, userPath);
|
|
const dest = join(ROOT, userPath);
|
|
if (copyIfExists(src, dest)) {
|
|
log(` Restored: ${userPath}`);
|
|
restored++;
|
|
}
|
|
}
|
|
|
|
// Restore user-added public/ files
|
|
const backupPublicDir = join(backupDir, 'public');
|
|
if (existsSync(backupPublicDir)) {
|
|
for (const file of readdirSync(backupPublicDir)) {
|
|
const alreadyRestored = USER_PATHS.some(p => p === `public/${file}`);
|
|
if (!alreadyRestored) {
|
|
cpSync(join(backupPublicDir, file), join(ROOT, 'public', file));
|
|
log(` Restored (user asset): public/${file}`);
|
|
restored++;
|
|
}
|
|
}
|
|
}
|
|
|
|
log(`${restored} item(s) restored.`);
|
|
|
|
// 8. Install dependencies
|
|
logStep('Installing dependencies...');
|
|
try {
|
|
execSync('pnpm install', { cwd: ROOT, stdio: 'inherit' });
|
|
} catch {
|
|
logError('pnpm install failed. You may need to run it manually.');
|
|
}
|
|
|
|
// 9. Clean up temp directory
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
|
|
// 10. Done
|
|
console.log('\n====================================');
|
|
console.log(` Updated to v${latestVersion}!`);
|
|
console.log('====================================');
|
|
console.log('\n Next steps:');
|
|
console.log(' 1. Open your vault in Obsidian');
|
|
console.log(' 2. Go to Astro Modular Settings > Advanced tab');
|
|
console.log(' 3. Click "Apply to config.ts" under "Apply all settings"');
|
|
console.log(' to write your saved settings to the new config.ts');
|
|
console.log(' 4. Run `pnpm dev` to verify everything works');
|
|
console.log('');
|
|
}
|
|
|
|
main().catch(err => {
|
|
logError(err.message);
|
|
process.exit(1);
|
|
});
|