super-productivity/packages/plugin-dev/scripts/build-all.js
Johannes Millan 02bc3e88e3 feat(two-way-sync): add plugin OAuth, two-way field sync, and remote issue deletion
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.

Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.

Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation
2026-03-22 13:02:41 +01:00

414 lines
12 KiB
JavaScript
Executable file

#!/usr/bin/env node
const { exec } = require('child_process');
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
const execAsync = promisify(exec);
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
};
function log(message, color = '') {
if (process.argv.includes('--silent')) return;
console.log(`${color}${message}${colors.reset}`);
}
// Recursive copy function that handles both files and directories
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
copyRecursive(path.join(src, entry), path.join(dest, entry));
}
} else {
fs.copyFileSync(src, dest);
}
}
// Plugin configurations
const plugins = [
{
name: 'procrastination-buster',
path: 'procrastination-buster',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/procrastination-buster',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
// Migrated built-in plugins
{
name: 'api-test-plugin',
path: 'api-test-plugin',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/api-test-plugin',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const files = [
'manifest.json',
'plugin.js',
'index.html',
'config-schema.json',
'icon.svg',
];
for (const file of files) {
const src = path.join(pluginPath, file);
const dest = path.join(targetDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
}
}
return 'Copied to assets';
},
},
{
name: 'sync-md',
path: 'sync-md',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
try {
// Try normal build first
await execAsync(`cd ${pluginPath} && npm run build`);
} catch (buildError) {
// If normal build fails, try emergency build
console.log(' Normal build failed, trying emergency build...');
await execAsync(`cd ${pluginPath} && node scripts/emergency-build.js`);
}
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/sync-md',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
{
name: 'yesterday-tasks-plugin',
path: 'yesterday-tasks-plugin',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/yesterday-tasks-plugin',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const files = ['manifest.json', 'plugin.js', 'index.html', 'icon.svg'];
for (const file of files) {
const src = path.join(pluginPath, file);
const dest = path.join(targetDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
}
}
return 'Copied to assets';
},
},
{
name: 'ai-productivity-prompts',
path: 'ai-productivity-prompts',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/ai-productivity-prompts',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
{
name: 'automations',
path: 'automations',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
// Copy to assets directory
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/automations',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
{
name: 'github-issue-provider',
path: 'github-issue-provider',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/github-issue-provider',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
{
name: 'voice-reminder',
path: 'voice-reminder',
needsInstall: false,
copyToAssets: true,
buildCommand: async (pluginPath) => {
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/voice-reminder',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const files = ['manifest.json', 'plugin.js', 'icon.svg'];
for (const file of files) {
const src = path.join(pluginPath, file);
const dest = path.join(targetDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest);
}
}
// Copy i18n directory
const i18nSrc = path.join(pluginPath, 'i18n');
if (fs.existsSync(i18nSrc)) {
copyRecursive(i18nSrc, path.join(targetDir, 'i18n'));
}
return 'Copied to assets';
},
},
{
name: 'clickup-issue-provider',
path: 'clickup-issue-provider',
needsInstall: true,
copyToAssets: true,
buildCommand: async (pluginPath) => {
await execAsync(`cd ${pluginPath} && npm run build`);
const targetDir = path.join(
__dirname,
'../../../src/assets/bundled-plugins/clickup-issue-provider',
);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const distPath = path.join(pluginPath, 'dist');
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath);
for (const file of files) {
const src = path.join(distPath, file);
const dest = path.join(targetDir, file);
copyRecursive(src, dest);
}
}
return 'Built and copied to assets';
},
},
// google-calendar-provider: disabled from bundled builds pending legal review.
// Source remains in packages/plugin-dev/google-calendar-provider/
// To re-enable: uncomment and add back to BUNDLED_PLUGIN_PATHS in plugin.service.ts
];
async function buildPlugin(plugin) {
const startTime = Date.now();
log(`\n📦 Building ${plugin.name}...`, colors.cyan);
try {
// Check if plugin directory exists
if (!fs.existsSync(plugin.path)) {
throw new Error(`Plugin directory not found: ${plugin.path}`);
}
// Install dependencies if needed
if (plugin.needsInstall) {
const packageJsonPath = path.join(plugin.path, 'package.json');
const nodeModulesPath = path.join(plugin.path, 'node_modules');
if (fs.existsSync(packageJsonPath)) {
log(` Installing dependencies...`, colors.yellow);
try {
// Try to install dependencies
await execAsync(`cd ${plugin.path} && npm install --include=dev`);
} catch (installError) {
// If install fails, check if node_modules exists and continue
if (fs.existsSync(nodeModulesPath)) {
log(
` Using existing dependencies (install failed but node_modules exists)`,
colors.yellow,
);
} else {
throw installError;
}
}
}
}
// Run build command
log(` Building...`, colors.yellow);
const result = await plugin.buildCommand(plugin.path);
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
log(`${plugin.name} - ${result} (${duration}s)`, colors.green);
return { plugin: plugin.name, success: true, duration };
} catch (error) {
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
log(`${plugin.name} - Build failed (${duration}s)`, colors.red);
if (error.stdout) {
log(` stdout: ${error.stdout}`, colors.red);
}
if (error.stderr) {
log(` stderr: ${error.stderr}`, colors.red);
}
return { plugin: plugin.name, success: false, error: error.message, duration };
}
}
async function buildAll() {
log('\n🚀 Building all plugins...', colors.bright);
const startTime = Date.now();
// Build plugins in parallel
const results = await Promise.all(plugins.map(buildPlugin));
// Summary
const totalDuration = ((Date.now() - startTime) / 1000).toFixed(1);
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
log('\n📊 Build Summary:', colors.bright);
log(` Total plugins: ${plugins.length}`);
log(` Successful: ${successful}`, colors.green);
if (failed > 0) {
log(` Failed: ${failed}`, colors.red);
}
log(` Total time: ${totalDuration}s`);
// List outputs
log('\n📁 Build outputs:', colors.bright);
// Check for minimal plugin zip
if (fs.existsSync('minimal-plugin.zip')) {
log(` • minimal-plugin.zip`);
}
// Check for other plugin outputs
for (const plugin of plugins.slice(1)) {
const distPath = path.join(plugin.path, 'dist');
if (fs.existsSync(distPath)) {
const pluginZip = path.join(distPath, 'plugin.zip');
if (fs.existsSync(pluginZip)) {
const stats = fs.statSync(pluginZip);
const size = (stats.size / 1024).toFixed(1);
log(`${plugin.path}/dist/plugin.zip (${size} KB)`);
} else {
log(`${plugin.path}/dist/`);
}
}
}
if (failed > 0) {
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
buildAll().catch((error) => {
log(`\n❌ Build failed: ${error.message}`, colors.red);
process.exit(1);
});
}
module.exports = { buildAll };