From 50975c19e0455e2f916eca8838f877b64c57570c Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 30 Apr 2026 09:35:59 +0200 Subject: [PATCH] Automate source resource rebuild for SF --- bin/sf-from-source | 6 +- scripts/copy-resources.cjs | 93 +++++++++++++++++------------ scripts/dev-cli.js | 88 +++++++++++++++++++-------- scripts/ensure-source-resources.cjs | 92 ++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 65 deletions(-) create mode 100644 scripts/ensure-source-resources.cjs diff --git a/bin/sf-from-source b/bin/sf-from-source index edf9e8583..f25bbb2d7 100755 --- a/bin/sf-from-source +++ b/bin/sf-from-source @@ -30,10 +30,14 @@ set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "$(readlink -f "${BASH_SOURCE[0]}")")" &>/dev/null && pwd) SF_SOURCE_ROOT=$(cd -- "$SCRIPT_DIR/.." &>/dev/null && pwd) +NODE_BIN=${SF_NODE_BIN:-node} export SF_BIN_PATH="$SCRIPT_DIR/sf-from-source" +export SF_CLI_PATH="${SF_CLI_PATH:-$SCRIPT_DIR/sf-from-source}" -exec node \ +"$NODE_BIN" "$SF_SOURCE_ROOT/scripts/ensure-source-resources.cjs" + +exec "$NODE_BIN" \ --import "$SF_SOURCE_ROOT/src/resources/extensions/sf/tests/resolve-ts.mjs" \ --experimental-strip-types \ --no-warnings \ diff --git a/scripts/copy-resources.cjs b/scripts/copy-resources.cjs index 4ff932917..33310d6c6 100644 --- a/scripts/copy-resources.cjs +++ b/scripts/copy-resources.cjs @@ -1,53 +1,70 @@ #!/usr/bin/env node -const { spawnSync } = require('child_process'); -const { copyFileSync, mkdirSync, readdirSync, rmSync } = require('fs'); -const { dirname, join } = require('path'); +const { spawnSync } = require("node:child_process"); +const { + copyFileSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} = require("node:fs"); +const { dirname, join } = require("node:path"); function copyNonTsFiles(srcDir, destDir) { - for (const entry of readdirSync(srcDir, { withFileTypes: true })) { - const srcPath = join(srcDir, entry.name); - const destPath = join(destDir, entry.name); + for (const entry of readdirSync(srcDir, { withFileTypes: true })) { + const srcPath = join(srcDir, entry.name); + const destPath = join(destDir, entry.name); - if (entry.isDirectory()) { - copyNonTsFiles(srcPath, destPath); - continue; - } + if (entry.isDirectory()) { + copyNonTsFiles(srcPath, destPath); + continue; + } - if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) { - continue; - } + if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) { + continue; + } - mkdirSync(dirname(destPath), { recursive: true }); + mkdirSync(dirname(destPath), { recursive: true }); - // Rewrite pi.extensions paths from .ts to .js in package.json files - // so they match the compiled output (tsc compiles index.ts → index.js - // but package.json is copied as-is). - if (entry.name === 'package.json') { - try { - const pkg = JSON.parse(require('fs').readFileSync(srcPath, 'utf-8')); - if (Array.isArray(pkg?.pi?.extensions)) { - pkg.pi.extensions = pkg.pi.extensions.map(ext => - ext.replace(/\.ts$/, '.js').replace(/\.tsx$/, '.js') - ); - require('fs').writeFileSync(destPath, JSON.stringify(pkg, null, 2) + '\n'); - continue; - } - } catch { /* fall through to plain copy */ } - } + // Rewrite pi.extensions paths from .ts to .js in package.json files + // so they match the compiled output (tsc compiles index.ts → index.js + // but package.json is copied as-is). + if (entry.name === "package.json") { + try { + const pkg = JSON.parse(readFileSync(srcPath, "utf-8")); + if (Array.isArray(pkg?.pi?.extensions)) { + pkg.pi.extensions = pkg.pi.extensions.map((ext) => + ext.replace(/\.ts$/, ".js").replace(/\.tsx$/, ".js"), + ); + writeFileSync(destPath, JSON.stringify(pkg, null, 2) + "\n"); + continue; + } + } catch { + /* fall through to plain copy */ + } + } - copyFileSync(srcPath, destPath); - } + copyFileSync(srcPath, destPath); + } } -rmSync('dist/resources', { recursive: true, force: true }); +rmSync("dist/resources", { recursive: true, force: true }); -const tscBin = require.resolve('typescript/bin/tsc'); -const compile = spawnSync(process.execPath, [tscBin, '--project', 'tsconfig.resources.json'], { - stdio: 'inherit', -}); +const tscBin = require.resolve("typescript/bin/tsc"); +const compile = spawnSync( + process.execPath, + [tscBin, "--project", "tsconfig.resources.json"], + { + stdio: "inherit", + }, +); if (compile.status !== 0) { - process.exit(compile.status ?? 1); + process.exit(compile.status ?? 1); } -copyNonTsFiles('src/resources', 'dist/resources'); +copyNonTsFiles("src/resources", "dist/resources"); +writeFileSync( + join("dist", "resources", ".sf-resource-build-stamp"), + `${new Date().toISOString()}\n`, +); diff --git a/scripts/dev-cli.js b/scripts/dev-cli.js index 12cc902cf..ddf1c28d2 100644 --- a/scripts/dev-cli.js +++ b/scripts/dev-cli.js @@ -1,33 +1,69 @@ #!/usr/bin/env node -import { spawn } from 'node:child_process' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' +import { spawn, spawnSync } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)) -const root = resolve(__dirname, '..') -const srcLoaderPath = resolve(root, 'src', 'loader.ts') -const resolveTsPath = resolve(root, 'src', 'resources', 'extensions', 'sf', 'tests', 'resolve-ts.mjs') +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, ".."); +const sourceBinPath = resolve(root, "bin", "sf-from-source"); +const ensureResourcesPath = resolve( + root, + "scripts", + "ensure-source-resources.cjs", +); +const srcLoaderPath = resolve(root, "src", "loader.ts"); +const resolveTsPath = resolve( + root, + "src", + "resources", + "extensions", + "sf", + "tests", + "resolve-ts.mjs", +); + +const resourceBuild = spawnSync(process.execPath, [ensureResourcesPath], { + cwd: root, + stdio: "inherit", + env: process.env, +}); + +if (resourceBuild.status !== 0) { + process.exit(resourceBuild.status ?? 1); +} const child = spawn( - process.execPath, - ['--import', resolveTsPath, '--experimental-strip-types', srcLoaderPath, ...process.argv.slice(2)], - { - cwd: process.cwd(), - stdio: 'inherit', - env: process.env, - }, -) + process.execPath, + [ + "--import", + resolveTsPath, + "--experimental-strip-types", + srcLoaderPath, + ...process.argv.slice(2), + ], + { + cwd: process.cwd(), + stdio: "inherit", + env: { + ...process.env, + SF_BIN_PATH: process.env.SF_BIN_PATH || sourceBinPath, + SF_CLI_PATH: process.env.SF_CLI_PATH || sourceBinPath, + }, + }, +); -child.on('error', (error) => { - console.error(`[forge] Failed to launch local dev CLI: ${error instanceof Error ? error.message : String(error)}`) - process.exit(1) -}) +child.on("error", (error) => { + console.error( + `[forge] Failed to launch local dev CLI: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); +}); -child.on('exit', (code, signal) => { - if (signal) { - process.kill(process.pid, signal) - return - } - process.exit(code ?? 0) -}) +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); +}); diff --git a/scripts/ensure-source-resources.cjs b/scripts/ensure-source-resources.cjs new file mode 100644 index 000000000..a857ee2b9 --- /dev/null +++ b/scripts/ensure-source-resources.cjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +const { spawnSync } = require("node:child_process"); +const { existsSync, readdirSync, statSync } = require("node:fs"); +const { join, resolve } = require("node:path"); + +const root = resolve(__dirname, ".."); +const srcResources = join(root, "src", "resources"); +const distResources = join(root, "dist", "resources"); +const stampPath = join(distResources, ".sf-resource-build-stamp"); +const copyResourcesScript = join(root, "scripts", "copy-resources.cjs"); + +function latestMtimeMs(path) { + let latest = 0; + const stack = [path]; + + while (stack.length > 0) { + const current = stack.pop(); + if (!current) continue; + + let entries; + try { + entries = readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + + for (const entry of entries) { + const entryPath = join(current, entry.name); + let stat; + try { + stat = statSync(entryPath); + } catch { + continue; + } + latest = Math.max(latest, stat.mtimeMs); + if (entry.isDirectory()) { + stack.push(entryPath); + } + } + } + + return latest; +} + +function sourceInputsMtimeMs() { + return Math.max( + latestMtimeMs(srcResources), + existsSync(copyResourcesScript) ? statSync(copyResourcesScript).mtimeMs : 0, + existsSync(join(root, "tsconfig.resources.json")) + ? statSync(join(root, "tsconfig.resources.json")).mtimeMs + : 0, + ); +} + +function hasCompleteResourceBuild() { + return ( + existsSync(stampPath) && + existsSync(join(distResources, "SF-WORKFLOW.md")) && + existsSync(join(distResources, "agents")) && + existsSync(join(distResources, "extensions")) + ); +} + +function shouldRebuild() { + if (process.env.SF_DEV_CLI_SKIP_RESOURCE_BUILD === "1") return false; + if (process.env.SF_SKIP_SOURCE_RESOURCE_BUILD === "1") return false; + if (!hasCompleteResourceBuild()) return true; + + let stampMtime = 0; + try { + stampMtime = statSync(stampPath).mtimeMs; + } catch { + return true; + } + + return sourceInputsMtimeMs() > stampMtime; +} + +if (shouldRebuild()) { + console.error( + "[forge] Source resources changed; rebuilding dist/resources before launch...", + ); + const result = spawnSync(process.execPath, [copyResourcesScript], { + cwd: root, + stdio: "inherit", + env: process.env, + }); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +}