Files
Orkas/scripts/prepare-win-native-deps.cjs
wyt baa9dd5ba0 Improve task-board controls and scoped connector authorization
Preserve keyboard focus and single activation across task-board refreshes. Add task-scoped grants, partial-permission recovery, validated CLI attachments, device-local project directories, and the public model catalog update.

Prompt confirmation:
  Confirmed-by: Requester in the current implementation task
  Scope: Apply the existing approved shared agent, skill, task, and connector
    capabilities within the documented open-source product boundaries.

Prompt audit:
- Keep: Commander and actor task identity, ordered execution, user input,
  project context, and tool-use responsibilities in the shared prompt
  templates and their existing assembly path. Type: resident.
  Why: These clauses determine behavior across ordinary turns; the update
  preserves the shared product's source-of-truth context and recovery
  ownership without adding an independent open-source policy layer.
- Keep: Project-task, scheduling, memory, connector setup, UI artifact,
  image/video composition, Office, and SEO workflows in their owning
  skills and referenced guides. Type: on-demand.
  Why: Load specialized procedures only for the corresponding work; retain
  source metadata and dependency ownership rather than duplicate those
  procedures in every actor prompt.
- Keep: Scoped tool schemas, queue/permission state, persisted recovery
  evidence, output storage, resource manifests, and deterministic validators
  at their existing host boundaries. Type: non-prompt enforcement.
  Why: Code and structured events own the invariants and prevent the prompt
  from becoming a second authorization, persistence, or execution engine.
- Drop: Hosted account, membership, cloud/relay, telemetry, internal debug,
  managed voice, watermark, welcome, and official updater instructions or
  affordances from the public execution paths. Type: source-only/on-demand.
  Why: Those paths require services absent from the open-source product;
  shared local operations and public API-key/BYO capabilities remain usable.

Validation: Recorded 11,721 deterministic tests, 538 resource tests, 1,248 native tests, five GUI checks and 198 reconciled desktop scenarios; final keyboard owner regressions also passed. Initial failed runs and classified warnings are retained in validation evidence.
2026-09-11 11:23:45 +08:00

273 lines
9.8 KiB
JavaScript

'use strict';
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { sharpPreparationFiles } = require('../bin/native-package-gate.cjs');
const {
ensureFile,
isPeX64,
markerMatches,
packagePath,
packageVersion,
readElectronVersion,
readLockPackage,
scriptHashes,
writeMarker,
} = require('./native-prepare-cache.cjs');
const PC_DIR = path.resolve(__dirname, '..');
const LOCK_FILE = path.join(PC_DIR, 'package-lock.json');
const CACHE_HELPER = path.join(__dirname, 'native-prepare-cache.cjs');
const TARGET_PLATFORM = 'win32';
const TARGET_ARCH = 'x64';
const WINDOWS_RM_OPTIONS = Object.freeze({
recursive: true,
force: true,
maxRetries: 6,
retryDelay: 100,
});
function npmCmd(platform = process.platform) {
return platform === 'win32' ? 'npm.cmd' : 'npm';
}
function removeTree(target, fsImpl = fs) {
fsImpl.rmSync(target, WINDOWS_RM_OPTIONS);
}
function run(cwd, command, args) {
const result = spawnSync(command, args, {
cwd,
stdio: 'inherit',
shell: false,
windowsHide: true,
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
}
}
function npmPack(cwd, spec) {
const result = spawnSync(npmCmd(), ['pack', spec, '--json'], {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'],
shell: process.platform === 'win32',
windowsHide: true,
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`npm pack ${spec} failed with exit code ${result.status}`);
}
const packs = JSON.parse(result.stdout);
const filename = packs?.[0]?.filename;
if (!filename) {
throw new Error(`npm pack ${spec} did not report an output filename`);
}
return path.join(cwd, filename);
}
function allFilesExist(files) {
return files.every((file) => fs.existsSync(file) && fs.statSync(file).isFile());
}
function extractTarballInTarget(targetDir, tarball, deps = {}) {
const fsImpl = deps.fsImpl || fs;
const runImpl = deps.runImpl || run;
// Cross-builds use host-native paths even though the extracted package is a
// Windows target. Keep Windows-shaped paths testable from a non-Windows host.
const pathImpl = path.win32.isAbsolute(targetDir) && !path.posix.isAbsolute(targetDir)
? path.win32
: path.posix.isAbsolute(targetDir) ? path.posix : path;
const archiveName = pathImpl.basename(tarball);
const localTarball = pathImpl.join(targetDir, archiveName);
fsImpl.copyFileSync(tarball, localTarball);
try {
// GNU tar treats a Windows absolute archive path (`C:\...`) as a remote
// host specification when release packaging runs under Git Bash/MSYS.
runImpl(targetDir, 'tar', ['-xzf', archiveName, '--strip-components=1']);
} finally {
fsImpl.rmSync(localTarball, { force: true });
}
}
function ensurePackageFromRegistry(packageName, requiredFiles = []) {
const version = readLockPackage(LOCK_FILE, packageName);
const targetDir = packagePath(PC_DIR, packageName);
if (packageVersion(PC_DIR, packageName) === version && allFilesExist(requiredFiles)) {
console.log(`[prepare-win-native-deps] reusing ${packageName}@${version}`);
return;
}
console.log(`[prepare-win-native-deps] ensuring ${packageName}@${version}`);
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orkas-win-native-deps-'));
try {
const tarball = npmPack(tmpDir, `${packageName}@${version}`);
removeTree(targetDir);
fs.mkdirSync(targetDir, { recursive: true });
extractTarballInTarget(targetDir, tarball);
} finally {
removeTree(tmpDir);
}
}
function expectedState(electronVersion) {
return {
schema: 1,
platform: TARGET_PLATFORM,
arch: TARGET_ARCH,
electronVersion,
scriptHashes: scriptHashes({
prepareWinNativeDeps: __filename,
nativePrepareCache: CACHE_HELPER,
nativePackageGate: require.resolve('../bin/native-package-gate.cjs'),
}),
packages: {
'@esbuild/win32-x64': readLockPackage(LOCK_FILE, '@esbuild/win32-x64'),
'sqlite-vec-windows-x64': readLockPackage(LOCK_FILE, 'sqlite-vec-windows-x64'),
'@napi-rs/canvas-win32-x64-msvc': readLockPackage(LOCK_FILE, '@napi-rs/canvas-win32-x64-msvc'),
'@anush008/tokenizers-win32-x64-msvc': readLockPackage(LOCK_FILE, '@anush008/tokenizers-win32-x64-msvc'),
'better-sqlite3': readLockPackage(LOCK_FILE, 'better-sqlite3'),
'@img/sharp-win32-x64': readLockPackage(LOCK_FILE, '@img/sharp-win32-x64'),
},
};
}
function removeLegacyMarker() {
removeTree(path.join(PC_DIR, 'node_modules', '.orkas-native-prepared'));
}
function removeDirectories(parentDir, shouldRemove, fsImpl = fs) {
if (!fsImpl.existsSync(parentDir) || !fsImpl.statSync(parentDir).isDirectory()) {
return;
}
for (const entry of fsImpl.readdirSync(parentDir, { withFileTypes: true })) {
if (entry.isDirectory() && shouldRemove(entry.name)) {
removeTree(path.join(parentDir, entry.name), fsImpl);
}
}
}
function main() {
const electronVersion = readElectronVersion(PC_DIR);
const state = expectedState(electronVersion);
const sharpVersion = state.packages['@img/sharp-win32-x64'];
const sharpFiles = sharpPreparationFiles('win32', 'x64', sharpVersion);
const sqliteVecDir = path.join(PC_DIR, 'node_modules', 'sqlite-vec');
const betterSqliteDir = path.join(PC_DIR, 'node_modules', 'better-sqlite3');
const betterSqliteBinary = path.join(betterSqliteDir, 'build', 'Release', 'better_sqlite3.node');
const required = {
esbuild: path.join(PC_DIR, 'node_modules', '@esbuild', 'win32-x64', 'esbuild.exe'),
sqliteVec: path.join(PC_DIR, 'node_modules', 'sqlite-vec-windows-x64', 'vec0.dll'),
betterSqlite: betterSqliteBinary,
canvas: path.join(PC_DIR, 'node_modules', '@napi-rs', 'canvas-win32-x64-msvc', 'skia.win32-x64-msvc.node'),
tokenizers: path.join(PC_DIR, 'node_modules', '@anush008', 'tokenizers-win32-x64-msvc', 'tokenizers.win32-x64-msvc.node'),
sharp: path.join(PC_DIR, 'node_modules', sharpFiles.binding),
sharpVipsCpp: path.join(PC_DIR, 'node_modules', sharpFiles.cpp),
sharpVips: path.join(PC_DIR, 'node_modules', sharpFiles.libvips),
};
if (!fs.existsSync(sqliteVecDir) || !fs.statSync(sqliteVecDir).isDirectory()) {
throw new Error(`sqlite-vec is not installed: ${sqliteVecDir}`);
}
if (!fs.existsSync(betterSqliteDir) || !fs.statSync(betterSqliteDir).isDirectory()) {
throw new Error(`better-sqlite3 is not installed: ${betterSqliteDir}`);
}
const prepared = markerMatches(PC_DIR, state, Object.values(required), () => [
betterSqliteBinary,
required.sharp,
required.sharpVipsCpp,
required.sharpVips,
].every(isPeX64));
removeLegacyMarker();
if (prepared) {
console.log(`[prepare-win-native-deps] using cached Windows native dependencies for Electron ${electronVersion}`);
return;
}
ensurePackageFromRegistry('@esbuild/win32-x64', [required.esbuild]);
ensurePackageFromRegistry('sqlite-vec-windows-x64', [required.sqliteVec]);
ensurePackageFromRegistry('@napi-rs/canvas-win32-x64-msvc', [required.canvas]);
ensurePackageFromRegistry('@anush008/tokenizers-win32-x64-msvc', [required.tokenizers]);
ensurePackageFromRegistry('@img/sharp-win32-x64', [required.sharp, required.sharpVipsCpp, required.sharpVips]);
if (packageVersion(PC_DIR, 'better-sqlite3') === state.packages['better-sqlite3'] && isPeX64(betterSqliteBinary)) {
console.log(`[prepare-win-native-deps] reusing better-sqlite3 Electron ${electronVersion} win32-x64 prebuild`);
} else {
console.log(`[prepare-win-native-deps] ensuring better-sqlite3 Electron ${electronVersion} win32-x64 prebuild`);
run(betterSqliteDir, process.execPath, [
require.resolve('prebuild-install/bin.js'),
'--runtime',
'electron',
'--target',
electronVersion,
'--platform',
TARGET_PLATFORM,
'--arch',
TARGET_ARCH,
'--force',
]);
}
console.log('[prepare-win-native-deps] pruning non-Windows native runtime packages');
removeDirectories(path.join(PC_DIR, 'node_modules', '@esbuild'), (name) => name !== 'win32-x64');
removeDirectories(path.join(PC_DIR, 'node_modules'), (name) => /^sqlite-vec-(darwin|linux)-/i.test(name));
removeDirectories(path.join(PC_DIR, 'node_modules', '@napi-rs'), (name) => /^canvas-/i.test(name) && name !== 'canvas-win32-x64-msvc');
removeDirectories(path.join(PC_DIR, 'node_modules', '@anush008'), (name) => /^tokenizers-/i.test(name) && name !== 'tokenizers-win32-x64-msvc');
removeDirectories(path.join(PC_DIR, 'node_modules', '@img'), (name) => (
(/^sharp-(darwin|linux|win32)-/i.test(name) && name !== 'sharp-win32-x64')
|| /^sharp-libvips-/i.test(name)
));
removeTree(path.join(PC_DIR, 'node_modules', 'fsevents'));
ensureFile(
'Windows esbuild runtime binary',
required.esbuild,
);
ensureFile(
'Windows sqlite-vec runtime binary',
required.sqliteVec,
);
ensureFile(
'Windows better-sqlite3 runtime binary',
required.betterSqlite,
);
if (!isPeX64(required.betterSqlite)) {
throw new Error(`Windows better-sqlite3 runtime binary is not PE x64: ${required.betterSqlite}`);
}
ensureFile(
'Windows canvas runtime binary',
required.canvas,
);
ensureFile(
'Windows tokenizers runtime binary',
required.tokenizers,
);
for (const [label, file] of [
['Windows sharp runtime binding', required.sharp],
['Windows sharp libvips C++ runtime', required.sharpVipsCpp],
['Windows sharp libvips runtime', required.sharpVips],
]) {
ensureFile(label, file);
if (!isPeX64(file)) throw new Error(`${label} is not PE x64: ${file}`);
}
writeMarker(PC_DIR, state);
}
module.exports = {
WINDOWS_RM_OPTIONS,
allFilesExist,
extractTarballInTarget,
npmCmd,
removeDirectories,
removeTree,
};
if (require.main === module) main();