mirror of
https://github.com/Heretek-AI/GDevelop-BYOK.git
synced 2026-07-01 18:48:04 -04:00
4a375b3f3d
Automated pipeline to watch GDevelop releases, patch source for BYOK AI support, unlock all premium features, and build for Windows/macOS/Linux. - GitHub Actions workflow checks for new releases every 30 minutes - patch.js applies 6 patches: premium capabilities, AI proxy reroute, watermark defaults, build quotas, preload proxy detection - AI proxy server (Express) implements GDevelop Generation API protocol with support for OpenAI, Anthropic, Google, OpenRouter, and Ollama - Local build script for manual testing D001-D003: architectural decisions captured in .gsd/DECISIONS.md
301 lines
12 KiB
JavaScript
301 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* patch.js — Applies BYOK + premium-unlock patches to a GDevelop source tree.
|
|
*
|
|
* Usage: node patch.js --release v5.6.269
|
|
* (runs against the GDevelop repo in the current directory)
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
const RELEASE_TAG = process.argv.includes('--release')
|
|
? process.argv[process.argv.indexOf('--release') + 1]
|
|
: 'unknown';
|
|
|
|
const ROOT = process.argv.includes('--repo')
|
|
? process.argv[process.argv.indexOf('--repo') + 1]
|
|
: process.cwd();
|
|
|
|
// ── Logger ─────────────────────────────────────────────────────────
|
|
function log(msg) {
|
|
console.log(`[patch] ${msg}`);
|
|
}
|
|
|
|
function patchFile(relativePath, replacements) {
|
|
const fullPath = path.join(ROOT, relativePath);
|
|
if (!fs.existsSync(fullPath)) {
|
|
log(`⚠️ SKIP: ${relativePath} not found`);
|
|
return false;
|
|
}
|
|
let content = fs.readFileSync(fullPath, 'utf8');
|
|
let changed = false;
|
|
|
|
for (const [search, replace] of replacements) {
|
|
if (content.includes(search)) {
|
|
content = content.replace(search, replace);
|
|
changed = true;
|
|
log(` ✓ ${relativePath}: ${search.substring(0, 60)}...`);
|
|
} else if (!content.includes(replace)) {
|
|
log(` ? ${relativePath}: pattern not found — ${search.substring(0, 60)}...`);
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
fs.writeFileSync(fullPath, content, 'utf8');
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
// ── 1. Unlock all premium capabilities ────────────────────────────
|
|
log('Patching Usage.js — premium unlock...');
|
|
patchFile('newIDE/app/src/Utils/GDevelopServices/Usage.js', [
|
|
// Patch hasValidSubscriptionPlan to always return true
|
|
[
|
|
`export const hasValidSubscriptionPlan = (
|
|
subscription: ?Subscription
|
|
): boolean => {
|
|
const hasValidSubscription = !!subscription && !!subscription.planId
|
|
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
|
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
|
if (hasValidSubscription) {
|
|
// The user has a subscription registered in the backend (classic "Registered" user).
|
|
return true;
|
|
}
|
|
return false;
|
|
};`,
|
|
`export const hasValidSubscriptionPlan = (
|
|
subscription: ?Subscription
|
|
): boolean => {
|
|
// BYOK PATCH: Always return true — all premium features unlocked.
|
|
return true;
|
|
};`,
|
|
],
|
|
]);
|
|
|
|
// Patch getUserLimits to return maxed-out capabilities
|
|
// We intercept the return value by wrapping ensureObjectHasProperty
|
|
// This is a broader patch — we replace the entire Limits response path
|
|
log('Patching Usage.js — max capabilities...');
|
|
const usageJsPath = 'newIDE/app/src/Utils/GDevelopServices/Usage.js';
|
|
if (fs.existsSync(path.join(ROOT, usageJsPath))) {
|
|
let usageContent = fs.readFileSync(path.join(ROOT, usageJsPath), 'utf8');
|
|
|
|
// Inject a function that returns maxed capabilities
|
|
const maxCapabilities = `
|
|
// BYOK PATCH: Returns maxed-out capabilities (all premium features unlocked)
|
|
const BYOK_MAX_CAPABILITIES = {
|
|
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
|
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
|
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
|
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
|
versionHistory: { enabled: true, retentionDays: 365 },
|
|
ai: { availablePresets: [] },
|
|
};
|
|
`;
|
|
|
|
usageContent = usageContent.replace(
|
|
'export const getUserLimits = async (',
|
|
maxCapabilities + '\nexport const getUserLimits = async ('
|
|
);
|
|
|
|
// Patch the return to return our maxed capabilities
|
|
// The function calls ensureObjectHasProperty on the response
|
|
// We intercept by replacing the function body
|
|
const getLimitsFnRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?return ensureObjectHasProperty\(\{[^}]+\}\s*\}\);\s*\};/;
|
|
if (getLimitsFnRegex.test(usageContent)) {
|
|
usageContent = usageContent.replace(getLimitsFnRegex,
|
|
`export const getUserLimits = async (
|
|
getAuthorizationHeader: () => Promise<string>,
|
|
userId: string
|
|
): Promise<Limits> => {
|
|
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
|
return {
|
|
quotas: {},
|
|
capabilities: BYOK_MAX_CAPABILITIES,
|
|
credits: {
|
|
userBalance: { amount: 999999 },
|
|
prices: {},
|
|
purchasableQuantities: {},
|
|
},
|
|
message: undefined,
|
|
};
|
|
};`
|
|
);
|
|
} else {
|
|
log(' ⚠️ Could not find getUserLimits function body for replacement — trying alternative...');
|
|
// Alternative: just find the function start and replace everything to the end
|
|
const altRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?\};/;
|
|
if (altRegex.test(usageContent)) {
|
|
usageContent = usageContent.replace(altRegex,
|
|
`export const getUserLimits = async (
|
|
getAuthorizationHeader: () => Promise<string>,
|
|
userId: string
|
|
): Promise<Limits> => {
|
|
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
|
return {
|
|
quotas: {},
|
|
capabilities: {
|
|
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
|
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
|
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
|
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
|
versionHistory: { enabled: true, retentionDays: 365 },
|
|
ai: { availablePresets: [] },
|
|
},
|
|
credits: {
|
|
userBalance: { amount: 999999 },
|
|
prices: {},
|
|
purchasableQuantities: {},
|
|
},
|
|
message: undefined,
|
|
};
|
|
};`
|
|
);
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(path.join(ROOT, usageJsPath), usageContent, 'utf8');
|
|
log(' ✓ getUserLimits patched');
|
|
}
|
|
|
|
// ── 2. Route AI to local proxy ─────────────────────────────────────
|
|
log('Patching ApiConfigs.js — AI proxy reroute...');
|
|
patchFile('newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js', [
|
|
// Point generation API to local proxy
|
|
[
|
|
`export const GDevelopGenerationApi = {
|
|
baseUrl: ((isDev
|
|
? 'https://api-dev.gdevelop.io/generation'
|
|
: 'https://api.gdevelop.io/generation'): string),
|
|
};`,
|
|
`export const GDevelopGenerationApi = {
|
|
// BYOK PATCH: Route AI requests through local proxy for BYOK support.
|
|
// Falls back to GDevelop API if proxy is unavailable.
|
|
baseUrl: (() => {
|
|
// Check if BYOK proxy override is set
|
|
if (typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__) {
|
|
return window.__BYOK_API_BASE_URL__;
|
|
}
|
|
return isDev
|
|
? 'https://api-dev.gdevelop.io/generation'
|
|
: 'https://api.gdevelop.io/generation';
|
|
})(),
|
|
};`,
|
|
],
|
|
]);
|
|
|
|
// Add BYOK proxy detection to electron preload
|
|
const preloadPath = 'newIDE/electron-app/app/scripts/preload.js';
|
|
if (fs.existsSync(path.join(ROOT, preloadPath))) {
|
|
let preload = fs.readFileSync(path.join(ROOT, preloadPath), 'utf8');
|
|
const byokInjection = `
|
|
// BYOK PATCH: Auto-detect local AI proxy
|
|
const { net } = require('electron');
|
|
try {
|
|
const http = require('http');
|
|
const probe = http.request({ host: '127.0.0.1', port: 11400, path: '/health', method: 'GET', timeout: 500 }, (res) => {
|
|
if (res.statusCode === 200) {
|
|
console.log('[BYOK] AI Proxy detected on localhost:11400');
|
|
window.__BYOK_API_BASE_URL__ = 'http://localhost:11400';
|
|
window.__BYOK_PROXY_AVAILABLE__ = true;
|
|
}
|
|
});
|
|
probe.on('error', () => { /* proxy not running */ });
|
|
probe.end();
|
|
} catch (e) { /* electron net not available */ }
|
|
`;
|
|
|
|
preload = preload.replace(
|
|
'// Expose protected methods',
|
|
byokInjection + '\n// Expose protected methods'
|
|
);
|
|
fs.writeFileSync(path.join(ROOT, preloadPath), preload, 'utf8');
|
|
log(' ✓ Preload BYOK proxy detection injected');
|
|
}
|
|
|
|
// ── 3. Remove watermark toggle restriction ─────────────────────────
|
|
log('Patching ProjectPropertiesDialog — watermark unlock...');
|
|
const projectsPropsDialogPath = 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js';
|
|
if (fs.existsSync(path.join(ROOT, projectsPropsDialogPath))) {
|
|
let ppdContent = fs.readFileSync(path.join(ROOT, projectsPropsDialogPath), 'utf8');
|
|
|
|
// Remove subscription check for watermark removal (multiple patterns possible)
|
|
const watermarkGates = [
|
|
/canRemoveWatermark\s*&&/g,
|
|
/showWatermark.*subscription/i,
|
|
/!hasValidSubscription.*watermark/i,
|
|
];
|
|
|
|
for (const gate of watermarkGates) {
|
|
if (gate.test(ppdContent)) {
|
|
log(` Found watermark gate pattern: ${gate}`);
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(path.join(ROOT, projectsPropsDialogPath), ppdContent, 'utf8');
|
|
}
|
|
|
|
// ── 4. Remove build quota checks ───────────────────────────────────
|
|
log('Patching ShareDialog — remove build quota...');
|
|
const shareDialogsGlob = 'newIDE/app/src/ExportAndShare/ShareDialog/';
|
|
const shareDir = path.join(ROOT, shareDialogsGlob);
|
|
if (fs.existsSync(shareDir)) {
|
|
const files = fs.readdirSync(shareDir).filter(f => f.endsWith('.js'));
|
|
for (const file of files) {
|
|
let content = fs.readFileSync(path.join(shareDir, file), 'utf8');
|
|
if (content.includes('limitReached') || content.includes('quota') || content.includes('maximumCount')) {
|
|
content = content.replace(/limitReached/g, 'false /* BYOK */');
|
|
content = content.replace(/current\s*[><=]+\s*max/g, 'false /* BYOK */');
|
|
fs.writeFileSync(path.join(shareDir, file), content, 'utf8');
|
|
log(` ✓ ${file} — quota checks neutered`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 5. Patch GDJS watermark defaults ───────────────────────────────
|
|
log('Patching GDJS export — watermark defaults...');
|
|
const gdjsIndexHtml = 'GDJS/Runtime/index.html';
|
|
if (fs.existsSync(path.join(ROOT, gdjsIndexHtml))) {
|
|
let html = fs.readFileSync(path.join(ROOT, gdjsIndexHtml), 'utf8');
|
|
// Default showWatermark to false
|
|
html = html.replace(
|
|
/"showWatermark"\s*:\s*true/g,
|
|
'"showWatermark": false'
|
|
);
|
|
html = html.replace(
|
|
/"showWatermark"\s*:\s*!0/g,
|
|
'"showWatermark": false'
|
|
);
|
|
fs.writeFileSync(path.join(ROOT, gdjsIndexHtml), html, 'utf8');
|
|
log(' ✓ Default showWatermark → false');
|
|
}
|
|
|
|
// Also patch C++ side (GDevelop.js) watermark default
|
|
const projectCpp = 'Core/GDCore/Project/Project.cpp';
|
|
if (fs.existsSync(path.join(ROOT, projectCpp))) {
|
|
let cpp = fs.readFileSync(path.join(ROOT, projectCpp), 'utf8');
|
|
cpp = cpp.replace(/SetShowWatermark\(true\)/g, 'SetShowWatermark(false) /* BYOK */');
|
|
cpp = cpp.replace(/showWatermark\s*=\s*true/g, 'showWatermark = false /* BYOK */');
|
|
fs.writeFileSync(path.join(ROOT, projectCpp), cpp, 'utf8');
|
|
log(' ✓ C++ watermark default → false');
|
|
}
|
|
|
|
// ── 6. Write version stamp ─────────────────────────────────────────
|
|
const stampPath = path.join(ROOT, '.gsd', 'BYOK_PATCHED');
|
|
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
|
|
fs.writeFileSync(stampPath, JSON.stringify({
|
|
version: RELEASE_TAG,
|
|
patchedAt: new Date().toISOString(),
|
|
patches: [
|
|
'premium-capabilities-unlock',
|
|
'byok-proxy-reroute',
|
|
'watermark-removal-unlock',
|
|
'build-quota-removal',
|
|
'gdjs-watermark-default-false',
|
|
],
|
|
}, null, 2));
|
|
|
|
log(`\n✅ Patching complete for ${RELEASE_TAG}`);
|