mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: bundle skills and tools into packaged whitelabel builds
Build-time: - loader.ts copies skill directories and tool binaries referenced in whitelabel.yaml into src/whitelabel-resources/ - Rewrites paths in baked config to use __WHITELABEL_RESOURCES__ sentinel - forge.config.ts conditionally includes whitelabel-resources in extraResource Runtime: - main.ts passes GOOSE_RESOURCES_PATH to renderer via appConfig - sessions.ts resolves __WHITELABEL_RESOURCES__ sentinel to actual resources path when building the whitelabel recipe Absolute paths (dev mode) pass through unchanged.
This commit is contained in:
@@ -11,3 +11,6 @@ src/bin/goose-npm/
|
||||
src/bin/temporal.db
|
||||
# Signing credentials
|
||||
.env.signing
|
||||
|
||||
# Whitelabel bundled assets (build artifact)
|
||||
src/whitelabel-resources/
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
const { resolve } = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const extraResource = ['src/bin', 'src/images', 'src/sandbox'];
|
||||
if (fs.existsSync(resolve(__dirname, 'src/whitelabel-resources'))) {
|
||||
extraResource.push('src/whitelabel-resources');
|
||||
}
|
||||
|
||||
let cfg = {
|
||||
asar: true,
|
||||
extraResource: ['src/bin', 'src/images', 'src/sandbox'],
|
||||
extraResource,
|
||||
icon: 'src/images/icon',
|
||||
// Windows specific configuration
|
||||
win32: {
|
||||
|
||||
@@ -618,6 +618,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
...appConfig,
|
||||
GOOSE_API_HOST: baseUrl,
|
||||
GOOSE_WORKING_DIR: workingDir,
|
||||
GOOSE_RESOURCES_PATH: app.isPackaged ? process.resourcesPath : process.cwd(),
|
||||
REQUEST_DIR: dir,
|
||||
GOOSE_BASE_URL_SHARE: baseUrlShare,
|
||||
GOOSE_VERSION: version,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AppEvents } from './constants/events';
|
||||
import { decodeRecipe, Recipe } from './recipe';
|
||||
import type { WhiteLabelConfig } from './whitelabel/types';
|
||||
import { DEFAULT_WHITELABEL_CONFIG } from './whitelabel/defaults';
|
||||
import { RESOURCES_PREFIX } from './whitelabel/constants';
|
||||
|
||||
function getWhiteLabelConfig(): WhiteLabelConfig {
|
||||
try {
|
||||
@@ -19,6 +20,12 @@ function getWhiteLabelConfig(): WhiteLabelConfig {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveResourcePath(p: string): string {
|
||||
if (!p.startsWith(RESOURCES_PREFIX)) return p;
|
||||
const resourcesPath = (window.appConfig?.get('GOOSE_RESOURCES_PATH') as string) || '';
|
||||
return p.replace(RESOURCES_PREFIX, resourcesPath + '/whitelabel-resources');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Recipe from the whitelabel config.
|
||||
* This replaces the default system prompt entirely — the agent becomes
|
||||
@@ -48,7 +55,7 @@ function buildWhiteLabelRecipe(): Recipe | undefined {
|
||||
for (const skill of defaults.skills) {
|
||||
lines.push(`## ${skill.name}`);
|
||||
lines.push(skill.description);
|
||||
lines.push(`Skill directory: \`${skill.path}\``);
|
||||
lines.push(`Skill directory: \`${resolveResourcePath(skill.path)}\``);
|
||||
lines.push(
|
||||
'Read the SKILL.md in this directory for detailed instructions before starting this type of work.'
|
||||
);
|
||||
@@ -63,7 +70,7 @@ function buildWhiteLabelRecipe(): Recipe | undefined {
|
||||
for (const tool of defaults.tools) {
|
||||
lines.push(`## \`${tool.name}\``);
|
||||
lines.push(tool.description);
|
||||
lines.push(`Binary: \`${tool.path}\``);
|
||||
lines.push(`Binary: \`${resolveResourcePath(tool.path)}\``);
|
||||
if (tool.env) {
|
||||
const envList = Object.entries(tool.env)
|
||||
.map(([k, v]) => ` - \`${k}\`: ${v || '(required)'}`)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Sentinel prefix used in baked config paths for bundled assets.
|
||||
* At runtime, replaced with the actual Electron resources path.
|
||||
*/
|
||||
export const RESOURCES_PREFIX = '__WHITELABEL_RESOURCES__';
|
||||
@@ -34,6 +34,78 @@ function deepMerge(target: any, source: any): any {
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { RESOURCES_PREFIX } from './constants';
|
||||
|
||||
function copyDirRecursive(src: string, dest: string): void {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursive(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle skills, tools, and other assets referenced by the whitelabel config
|
||||
* into src/whitelabel-resources/ so they get picked up by extraResource.
|
||||
* Rewrites paths in the config to use RESOURCES_PREFIX.
|
||||
*/
|
||||
function bundleAssets(config: WhiteLabelConfig, configDir: string, projectRoot: string): void {
|
||||
const resourcesDir = path.join(projectRoot, 'src', 'whitelabel-resources');
|
||||
|
||||
// Clean previous bundle
|
||||
if (fs.existsSync(resourcesDir)) {
|
||||
fs.rmSync(resourcesDir, { recursive: true });
|
||||
}
|
||||
|
||||
let needsBundle = false;
|
||||
|
||||
// Bundle skill directories
|
||||
if (config.defaults.skills) {
|
||||
for (const skill of config.defaults.skills) {
|
||||
const resolved = path.resolve(configDir, skill.path);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.warn(`[WhiteLabel] Skill path not found: ${resolved}`);
|
||||
continue;
|
||||
}
|
||||
const destName = path.basename(resolved);
|
||||
const dest = path.join(resourcesDir, 'skills', destName);
|
||||
console.log(`[WhiteLabel] Bundling skill: ${resolved} → ${dest}`);
|
||||
copyDirRecursive(resolved, dest);
|
||||
skill.path = `${RESOURCES_PREFIX}/skills/${destName}`;
|
||||
needsBundle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Bundle tool binaries
|
||||
if (config.defaults.tools) {
|
||||
for (const tool of config.defaults.tools) {
|
||||
const resolved = path.resolve(configDir, tool.path);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.warn(`[WhiteLabel] Tool binary not found: ${resolved}`);
|
||||
continue;
|
||||
}
|
||||
const destName = path.basename(resolved);
|
||||
const dest = path.join(resourcesDir, 'tools', destName);
|
||||
console.log(`[WhiteLabel] Bundling tool: ${resolved} → ${dest}`);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(resolved, dest);
|
||||
// Preserve executable permission
|
||||
fs.chmodSync(dest, 0o755);
|
||||
tool.path = `${RESOURCES_PREFIX}/tools/${destName}`;
|
||||
needsBundle = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsBundle && fs.existsSync(resourcesDir)) {
|
||||
fs.rmSync(resourcesDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function loadWhiteLabelConfig(projectRoot: string): WhiteLabelConfig {
|
||||
// Allow override via env var — resolve relative paths from CWD (not projectRoot)
|
||||
const envPath = process.env.WHITELABEL_CONFIG;
|
||||
@@ -49,6 +121,11 @@ export function loadWhiteLabelConfig(projectRoot: string): WhiteLabelConfig {
|
||||
console.log(`[WhiteLabel] Loading config from ${configPath}`);
|
||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
||||
const parsed = yaml.parse(raw) as Partial<WhiteLabelConfig>;
|
||||
const config = deepMerge(DEFAULT_WHITELABEL_CONFIG, parsed) as WhiteLabelConfig;
|
||||
|
||||
return deepMerge(DEFAULT_WHITELABEL_CONFIG, parsed) as WhiteLabelConfig;
|
||||
// Bundle assets referenced by the config (skills, tools)
|
||||
const configDir = path.dirname(configPath);
|
||||
bundleAssets(config, configDir, projectRoot);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user