mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 17:05:48 +00:00
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
414 lines
12 KiB
JavaScript
Executable file
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 };
|