snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

1
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
tmp-*

View File

@@ -0,0 +1,46 @@
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const slugify = require("slugify");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
async function createMigration() {
try {
const migrationName = await new Promise((resolve) => {
rl.question("Enter migration name: ", resolve);
});
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, "../crates/yaak-models/migrations");
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
fs.writeFileSync(filePath, "-- Add migration SQL here\n");
console.log(`Created migration file: ${fileName}`);
} catch (error) {
console.error("Error creating migration:", error);
} finally {
rl.close();
}
}
createMigration().catch(console.error);

View File

@@ -0,0 +1,77 @@
const fs = require("node:fs");
const path = require("node:path");
const tar = require("tar");
const yauzl = require("yauzl");
// Resolve an archive entry against destDir, refusing anything that escapes it.
function safeJoin(destDir, entryName) {
const root = path.resolve(destDir);
const resolved = path.resolve(root, entryName);
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
throw new Error(`Archive entry escapes destination directory: ${entryName}`);
}
return resolved;
}
function extractZip(filePath, destDir) {
return new Promise((resolve, reject) => {
yauzl.open(filePath, { lazyEntries: true }, (err, zip) => {
if (err) return reject(err);
zip.on("error", reject);
zip.on("end", resolve);
zip.on("entry", (entry) => {
let dst;
try {
dst = safeJoin(destDir, entry.fileName);
} catch (e) {
return reject(e);
}
// Unix mode lives in the high 16 bits of the external attributes
const rawMode = (entry.externalFileAttributes >>> 16) & 0xffff;
const isSymlink = (rawMode & 0o170000) === 0o120000;
if (isSymlink) {
return reject(new Error(`Refusing to extract symlink from archive: ${entry.fileName}`));
}
if (entry.fileName.endsWith("/")) {
fs.mkdirSync(dst, { recursive: true });
return zip.readEntry();
}
zip.openReadStream(entry, (err2, stream) => {
if (err2) return reject(err2);
fs.mkdirSync(path.dirname(dst), { recursive: true });
const out = fs.createWriteStream(dst);
stream.on("error", reject);
out.on("error", reject);
out.on("close", () => {
const mode = rawMode & 0o7777;
if (mode !== 0) fs.chmodSync(dst, mode);
zip.readEntry();
});
stream.pipe(out);
});
});
zip.readEntry();
});
});
}
/**
* Extract a `.zip` or `.tar.gz` archive into destDir, preserving file modes.
* Entries that would land outside destDir are rejected.
*/
async function extractArchive(filePath, destDir) {
fs.mkdirSync(destDir, { recursive: true });
if (filePath.endsWith(".zip")) {
await extractZip(filePath, destDir);
} else if (filePath.endsWith(".tar.gz") || filePath.endsWith(".tgz")) {
// oxlint-disable-next-line await-thenable -- tar.x() returns a promise when `file` is set
await tar.x({ file: filePath, cwd: destDir });
} else {
throw new Error(`Unsupported archive format: ${path.basename(filePath)}`);
}
}
module.exports = { extractArchive };

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env node
/**
* Git post-checkout hook for auto-configuring worktree environments.
* This runs after 'git checkout' or 'git worktree add'.
*
* Args from git:
* process.argv[2] - previous HEAD ref
* process.argv[3] - new HEAD ref
* process.argv[4] - flag (1 = branch checkout, 0 = file checkout)
*/
import fs from "fs";
import path from "path";
import { execSync, execFileSync } from "child_process";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Git supplies three arguments when invoking this as a post-checkout hook. When run directly,
// assume the caller wants to configure the current worktree instead of requiring placeholder refs.
const isManualRun = process.argv.length === 2;
const isBranchCheckout = isManualRun || process.argv[4] === "1";
if (!isBranchCheckout) {
process.exit(0);
}
// Check if we're in a worktree by looking for .git file (not directory)
const gitPath = path.join(process.cwd(), ".git");
const isWorktree = fs.existsSync(gitPath) && fs.statSync(gitPath).isFile();
if (!isWorktree) {
process.exit(0);
}
const envLocalPath = path.join(process.cwd(), ".env.local");
// Don't overwrite existing .env.local
if (fs.existsSync(envLocalPath)) {
process.exit(0);
}
console.log("Detected new worktree - configuring ports in .env.local");
const CLIENT_PORT_BASE = 1420;
const PROXY_PORT_BASE = 2420; // Keep proxy +1000 from client to avoid cross-app conflicts
// Find the highest worktree index and MCP port in use across all worktrees.
// Main worktree (first in list) is assumed to use index 0:
// client=1420, proxy=2420, mcp=64343.
let maxMcpPort = 64343;
let maxWorktreeIndex = 0;
try {
const worktreeList = execSync("git worktree list --porcelain", {
encoding: "utf8",
});
const worktreePaths = worktreeList
.split("\n")
.filter((line) => line.startsWith("worktree "))
.map((line) => line.replace("worktree ", "").trim());
// Skip the first worktree (main) since it uses default ports
for (let i = 1; i < worktreePaths.length; i++) {
const worktreePath = worktreePaths[i];
const envPath = path.join(worktreePath, ".env.local");
if (fs.existsSync(envPath)) {
const content = fs.readFileSync(envPath, "utf8");
const mcpMatch = content.match(/^YAAK_PLUGIN_MCP_SERVER_PORT=(\d+)/m);
if (mcpMatch) {
const port = parseInt(mcpMatch[1], 10);
if (port > maxMcpPort) {
maxMcpPort = port;
}
}
const clientDevMatch = content.match(/^(?:YAAK_CLIENT_DEV_PORT|YAAK_DEV_PORT)=(\d+)/m);
if (clientDevMatch) {
const port = parseInt(clientDevMatch[1], 10);
const index = port - CLIENT_PORT_BASE;
if (index > maxWorktreeIndex) {
maxWorktreeIndex = index;
}
}
const proxyDevMatch = content.match(/^YAAK_PROXY_DEV_PORT=(\d+)/m);
if (proxyDevMatch) {
const port = parseInt(proxyDevMatch[1], 10);
const index = port - PROXY_PORT_BASE;
if (index > maxWorktreeIndex) {
maxWorktreeIndex = index;
}
}
}
}
// Increment MCP to get the next available port
maxMcpPort++;
} catch (err) {
console.error("Warning: Could not check other worktrees for port conflicts:", err.message);
// Continue with default ports
}
const nextWorktreeIndex = maxWorktreeIndex + 1;
const nextClientDevPort = CLIENT_PORT_BASE + nextWorktreeIndex;
const nextProxyDevPort = PROXY_PORT_BASE + nextWorktreeIndex;
// Get worktree name from current directory
const worktreeName = path.basename(process.cwd());
// Create .env.local with unique ports
const envContent = `# Auto-generated by git post-checkout hook
# This file configures ports for this worktree to avoid conflicts
# Vite dev server port for the client app (main worktree uses ${CLIENT_PORT_BASE})
YAAK_CLIENT_DEV_PORT=${nextClientDevPort}
# Vite dev server port for the proxy app (main worktree uses ${PROXY_PORT_BASE})
YAAK_PROXY_DEV_PORT=${nextProxyDevPort}
# MCP Server port (main worktree uses 64343)
YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}
`;
fs.writeFileSync(envLocalPath, envContent, "utf8");
console.log(
`Created .env.local with YAAK_CLIENT_DEV_PORT=${nextClientDevPort}, YAAK_PROXY_DEV_PORT=${nextProxyDevPort}, and YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}`,
);
// Create tauri.worktree.conf.json with unique app identifier for complete isolation
// This gives each worktree its own app data directory, avoiding the need for DB path prefixes
const clientTauriWorktreeConfig = {
identifier: `app.yaak.desktop.dev.${worktreeName}`,
productName: `Daak (${worktreeName})`,
};
const clientTauriConfigPath = path.join(
process.cwd(),
"crates-tauri",
"yaak-app-client",
"tauri.worktree.conf.json",
);
fs.writeFileSync(
clientTauriConfigPath,
JSON.stringify(clientTauriWorktreeConfig, null, 2) + "\n",
"utf8",
);
console.log(
`Created client tauri.worktree.conf.json with identifier: ${clientTauriWorktreeConfig.identifier}`,
);
const proxyTauriWorktreeConfig = {
identifier: `app.yaak.proxy.dev.${worktreeName}`,
productName: `Yaak Proxy (${worktreeName})`,
};
const proxyTauriConfigPath = path.join(
process.cwd(),
"crates-tauri",
"yaak-app-proxy",
"tauri.worktree.conf.json",
);
fs.writeFileSync(
proxyTauriConfigPath,
JSON.stringify(proxyTauriWorktreeConfig, null, 2) + "\n",
"utf8",
);
console.log(
`Created proxy tauri.worktree.conf.json with identifier: ${proxyTauriWorktreeConfig.identifier}`,
);
// Copy gitignored editor config folders from main worktree (.zed, .vscode, .claude, etc.)
// This ensures your editor settings, tasks, and configurations are available in the new worktree
// without needing to manually copy them or commit them to git.
try {
const worktreeList = execSync("git worktree list --porcelain", {
encoding: "utf8",
});
const mainWorktreePath = worktreeList
.split("\n")
.find((line) => line.startsWith("worktree "))
?.replace("worktree ", "")
.trim();
if (mainWorktreePath) {
// Find all .* folders in main worktree root that are gitignored
const entries = fs.readdirSync(mainWorktreePath, { withFileTypes: true });
const dotFolders = entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith("."))
.map((entry) => entry.name);
for (const folder of dotFolders) {
const sourcePath = path.join(mainWorktreePath, folder);
const destPath = path.join(process.cwd(), folder);
try {
// Check if it's gitignored - run from main worktree directory
execFileSync("git", ["check-ignore", "-q", folder], {
stdio: "pipe",
cwd: mainWorktreePath,
});
// It's gitignored, copy it
fs.cpSync(sourcePath, destPath, { recursive: true });
console.log(`Copied ${folder} from main worktree`);
} catch {
// Not gitignored or doesn't exist, skip
}
}
}
} catch (err) {
console.warn("Warning: Could not copy files from main worktree:", err.message);
// Continue anyway
}
console.log("\n✓ Worktree setup complete! Run `npm run init` to install dependencies.");

View File

@@ -0,0 +1,18 @@
const { execSync } = require("node:child_process");
const version = tryExecSync("wasm-pack --version");
if (version.startsWith("wasm-pack ")) {
console.log("wasm-pack already installed");
return;
}
console.log("Installing wasm-pack via cargo...");
execSync("cargo install wasm-pack --locked", { stdio: "inherit" });
function tryExecSync(cmd) {
try {
return execSync(cmd, { stdio: "pipe" }).toString("utf-8");
} catch {
return "";
}
}

View File

@@ -0,0 +1,19 @@
const { readdirSync } = require("node:fs");
const path = require("node:path");
const { execSync } = require("node:child_process");
const pluginsDir = path.join(__dirname, "..", "plugins");
console.log("Publishing core Yaak plugins");
for (const name of readdirSync(pluginsDir)) {
const dir = path.join(pluginsDir, name);
if (name.startsWith(".")) continue;
console.log("Building plugin", dir);
execSync("npm run build", { stdio: "inherit", cwd: dir });
execSync("yaakcli publish", {
stdio: "inherit",
cwd: dir,
env: { ...process.env, ENVIRONMENT: "development" },
});
}

View File

@@ -0,0 +1,15 @@
const path = require("path");
const fs = require("fs");
const version = process.env.YAAK_VERSION?.replace("v", "");
if (!version) {
throw new Error("YAAK_VERSION environment variable not set");
}
const tauriConfigPath = path.join(__dirname, "../crates-tauri/yaak-app-client/tauri.conf.json");
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, "utf8"));
tauriConfig.version = version;
console.log("Writing version " + version + " to " + tauriConfigPath);
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2));

62
scripts/run-build.mjs Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Runs a Tauri app build from the app directory so relative config paths and
* beforeBuildCommand entries resolve consistently.
*/
import { spawnSync } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, "..");
const [appName = "client", ...additionalArgs] = process.argv.slice(2);
const appConfigs = {
client: {
appDir: "crates-tauri/yaak-app-client",
tauriConfig: "tauri.conf.json",
},
proxy: {
appDir: "crates-tauri/yaak-app-proxy",
tauriConfig: "tauri.conf.json",
},
};
const appConfig = appConfigs[appName];
if (appConfig == null) {
console.error(`Unknown Tauri app "${appName}"`);
process.exit(1);
}
// Normalize extra `--config` path args to absolute paths from repo root so
// callers can keep passing root-relative config files.
const normalizedAdditionalArgs = [];
for (let i = 0; i < additionalArgs.length; i++) {
const arg = additionalArgs[i];
if (arg === "--config" && i + 1 < additionalArgs.length) {
const value = additionalArgs[i + 1];
const isInlineJson = value.trimStart().startsWith("{");
normalizedAdditionalArgs.push("--config");
normalizedAdditionalArgs.push(
!isInlineJson && !path.isAbsolute(value) ? path.join(rootDir, value) : value,
);
i++;
continue;
}
normalizedAdditionalArgs.push(arg);
}
const tauriJs = path.join(rootDir, "node_modules", "@tauri-apps", "cli", "tauri.js");
const result = spawnSync(
process.execPath,
[tauriJs, "build", "--config", appConfig.tauriConfig, ...normalizedAdditionalArgs],
{
cwd: path.join(rootDir, appConfig.appDir),
stdio: "inherit",
env: process.env,
},
);
process.exit(result.status || 0);

121
scripts/run-dev.mjs Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* Script to run a Tauri app dev server with dynamic port configuration.
*/
import { spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, "..");
// Load .env.local if it exists
const envLocalPath = path.join(rootDir, ".env.local");
if (fs.existsSync(envLocalPath)) {
const envContent = fs.readFileSync(envLocalPath, "utf8");
const envVars = envContent
.split("\n")
.filter((line) => line && !line.startsWith("#"))
.reduce((acc, line) => {
const [key, value] = line.split("=");
if (key && value) {
acc[key.trim()] = value.trim();
}
return acc;
}, {});
Object.assign(process.env, envVars);
}
const [appName = "client", ...additionalArgs] = process.argv.slice(2);
const appConfigs = {
client: {
appDir: "crates-tauri/yaak-app-client",
devPortEnv: "YAAK_CLIENT_DEV_PORT",
fallbackDevPortEnv: "YAAK_DEV_PORT",
defaultPort: "1420",
tauriConfig: "tauri.conf.json",
tauriDevConfig: "tauri.development.conf.json",
tauriWorktreeConfig: "tauri.worktree.conf.json",
},
proxy: {
appDir: "crates-tauri/yaak-app-proxy",
devPortEnv: "YAAK_PROXY_DEV_PORT",
fallbackDevPortEnv: null,
defaultPort: "2420",
tauriConfig: "tauri.conf.json",
tauriDevConfig: "tauri.development.conf.json",
tauriWorktreeConfig: "tauri.worktree.conf.json",
},
};
const appConfig = appConfigs[appName];
if (appConfig == null) {
console.error(`Unknown Tauri app "${appName}"`);
process.exit(1);
}
const port =
process.env[appConfig.devPortEnv] ||
(appConfig.fallbackDevPortEnv ? process.env[appConfig.fallbackDevPortEnv] : undefined) ||
appConfig.defaultPort;
const config = JSON.stringify({
build: { devUrl: `http://localhost:${port}` },
});
// Normalize extra `--config` path args to absolute paths from repo root so
// callers can keep passing root-relative config files.
const normalizedAdditionalArgs = [];
for (let i = 0; i < additionalArgs.length; i++) {
const arg = additionalArgs[i];
if (arg === "--") {
normalizedAdditionalArgs.push(arg, ...additionalArgs.slice(i + 1));
break;
}
if (arg === "--config" && i + 1 < additionalArgs.length) {
const value = additionalArgs[i + 1];
const isInlineJson = value.trimStart().startsWith("{");
normalizedAdditionalArgs.push("--config");
normalizedAdditionalArgs.push(
!isInlineJson && !path.isAbsolute(value) ? path.join(rootDir, value) : value,
);
i++;
continue;
}
normalizedAdditionalArgs.push(arg);
}
// Tauri only auto-merges platform-specific config names, so the per-worktree config
// written by the post-checkout hook has to be passed explicitly. It's gitignored, so
// it's normally absent in the main checkout.
const worktreeConfigArgs = [];
if (fs.existsSync(path.join(rootDir, appConfig.appDir, appConfig.tauriWorktreeConfig))) {
worktreeConfigArgs.push("--config", appConfig.tauriWorktreeConfig);
}
const args = [
"dev",
"--no-watch",
"--config",
appConfig.tauriConfig,
"--config",
appConfig.tauriDevConfig,
...worktreeConfigArgs,
"--config",
config,
...normalizedAdditionalArgs,
];
// Invoke the tauri CLI JS entry point directly via node to avoid shell escaping issues on Windows
const tauriJs = path.join(rootDir, "node_modules", "@tauri-apps", "cli", "tauri.js");
const result = spawnSync(process.execPath, [tauriJs, ...args], {
cwd: path.join(rootDir, appConfig.appDir),
stdio: "inherit",
env: process.env,
});
process.exit(result.status || 0);

120
scripts/run-web.mjs Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* Run Yaak in a browser: the client built for the web target, and the server
* that executes its sends.
*
* `YAAK_TARGET=web` is what resolves `@yaakapp-internal/platform` to its browser
* entry (see apps/yaak-client/vite.config.ts). Setting it in a node script rather
* than inline in an npm script keeps this working on Windows.
*/
import { spawn, spawnSync } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, "..");
// The post-checkout hook writes a per-worktree dev port in here, and the client's
// vite config reads YAAK_CLIENT_DEV_PORT from it. Without this, two worktrees both
// try to serve on 1420. (run-dev.mjs reads the same file, but with Object.assign,
// so there an explicit YAAK_CLIENT_DEV_PORT is silently ignored.)
const envLocalPath = path.join(rootDir, ".env.local");
if (fs.existsSync(envLocalPath)) {
for (const line of fs.readFileSync(envLocalPath, "utf8").split("\n")) {
if (!line || line.startsWith("#")) continue;
const [key, value] = line.split("=");
if (!key || !value) continue;
// Defaults, not overrides: a variable set on the command line is the more
// deliberate of the two and has to win, or `PORT=x vp run web:dev` silently
// lands somewhere else.
process.env[key.trim()] ??= value.trim();
}
}
const DIST = "dist/apps/yaak-client";
const [mode] = process.argv.slice(2);
// Invoke the Vite+ CLI JS entry point directly via node, the way run-dev.mjs invokes
// the Tauri CLI. The `.bin/vp` shim is a POSIX script that npm pairs with `vp.cmd` and
// `vp.ps1` on Windows, so spawning it without a shell only works on one platform.
const vp = path.join(rootDir, "node_modules", "vite-plus", "bin", "vp");
const runVite = (args) => [process.execPath, [vp, ...args]];
const env = (extra = {}) => ({ ...process.env, YAAK_TARGET: "web", ...extra });
/** Run a build step to completion. A failure ends the script. */
function run(command, args, extra = {}) {
const result = spawnSync(command, args, { cwd: rootDir, stdio: "inherit", env: env(extra) });
if (result.status !== 0) process.exit(result.status ?? 1);
}
/**
* Run a server until it stops, and take it down when this process is stopped.
*
* Servers are started as a direct child rather than through `cargo run`, and
* signals are forwarded to them, because neither happens by itself: `cargo run`
* is a wrapper that does not pass a terminating signal on to the binary it
* spawned, and a child is not killed by its parent exiting. Either one on its
* own leaves a server holding the port after the terminal that started it has
* gone, and the next run fails with "address already in use".
*/
function serve(command, args, extra = {}) {
const child = spawn(command, args, { cwd: rootDir, stdio: "inherit", env: env(extra) });
const stop = (signal) => child.killed || child.kill(signal);
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => stop(signal));
process.on("exit", () => stop("SIGTERM"));
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
child.on("error", (err) => {
console.error(`Failed to start ${command}: ${err.message}`);
process.exit(1);
});
}
/** Build the server and hand back the binary, so it can be run without a wrapper. */
function buildServer() {
run("cargo", ["build", "-p", "yaak-web"]);
const target = process.env.CARGO_TARGET_DIR ?? path.join(rootDir, "target");
return path.join(target, "debug", "yaak-web");
}
switch (mode) {
// The app, with hot reload, on one origin: its own `/v1` is passed through to
// the server `web:dev` starts alongside it. Nothing to build first and one
// address to open.
case "dev":
serve(...runVite(["-C", "apps/yaak-client", "dev", "--force"]));
break;
// The send executor behind the dev server, on the port a dev build looks for.
case "proxy":
serve(buildServer(), [], {
// So opening the send server's port in a browser lands on the app instead
// of an explanation of why the app is not there.
YAAK_WEB_APP_PORT: process.env.YAAK_CLIENT_DEV_PORT ?? process.env.YAAK_DEV_PORT ?? "1420",
});
break;
case "build":
run(...runVite(["-C", "apps/yaak-client", "build"]));
break;
// One process serving both, the shape the Docker image runs. The build comes
// first because serving a stale `dist` silently tests the last change but one.
case "serve": {
const server = buildServer();
run(...runVite(["-C", "apps/yaak-client", "build"]));
serve(server, ["--serve", DIST]);
break;
}
default:
console.error(`Unknown mode ${JSON.stringify(mode)}; expected dev, proxy, build or serve`);
process.exit(1);
}

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Runs `npm run dev` in parallel for the provided workspaces, or all
* workspaces with a dev script when none are specified.
* Handles cleanup of child processes on exit.
*/
import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, "..");
// Read root package.json to get workspaces
const rootPkg = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
const workspaces = rootPkg.workspaces || [];
const requestedWorkspaces = process.argv.slice(2);
const workspaceCandidates = requestedWorkspaces.length > 0 ? requestedWorkspaces : workspaces;
// Find all candidate workspaces with a dev script
const workspacesWithDev = workspaceCandidates.filter((ws) => {
const pkgPath = path.join(rootDir, ws, "package.json");
if (!fs.existsSync(pkgPath)) return false;
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
return pkg.scripts?.dev != null;
});
if (workspacesWithDev.length === 0) {
console.log("No workspaces with dev script found");
process.exit(0);
}
console.log(`Starting dev for ${workspacesWithDev.length} workspaces...`);
const children = [];
// Spawn all dev processes
for (const ws of workspacesWithDev) {
const cwd = path.join(rootDir, ws);
const child = spawn("npm", ["run", "dev"], {
cwd,
stdio: "inherit",
shell: process.platform === "win32",
});
child.on("error", (err) => {
console.error(`Error in ${ws}:`, err.message);
});
children.push({ ws, child });
}
// Cleanup function to kill all children
function cleanup() {
for (const { ws: _ws, child } of children) {
if (child.exitCode === null) {
// Process still running
if (process.platform === "win32") {
spawn("taskkill", ["/pid", child.pid, "/f", "/t"], { shell: true });
} else {
child.kill("SIGTERM");
}
}
}
}
// Handle various exit signals
process.on("SIGINT", () => {
cleanup();
process.exit(0);
});
process.on("SIGTERM", () => {
cleanup();
process.exit(0);
});
process.on("exit", cleanup);
// Keep the process running
process.stdin.resume();

114
scripts/vendor-node.cjs Normal file
View File

@@ -0,0 +1,114 @@
const path = require("node:path");
const crypto = require("node:crypto");
const fs = require("node:fs");
const Downloader = require("nodejs-file-downloader");
const { extractArchive } = require("./extract-archive.cjs");
const { rmSync, cpSync, mkdirSync, existsSync } = require("node:fs");
const { execSync } = require("node:child_process");
const nodeVersionFile = path.join(__dirname, "..", "packages", "plugin-runtime", ".node-version");
const NODE_VERSION = `v${fs.readFileSync(nodeVersionFile, "utf8").trim().replace(/^v/, "")}`;
// `${process.platform}_${process.arch}`
const MAC_ARM = "darwin_arm64";
const MAC_X64 = "darwin_x64";
const LNX_ARM = "linux_arm64";
const LNX_X64 = "linux_x64";
const WIN_X64 = "win32_x64";
const WIN_ARM = "win32_arm64";
const URL_MAP = {
[MAC_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-arm64.tar.gz`,
[MAC_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-darwin-x64.tar.gz`,
[LNX_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz`,
[LNX_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz`,
[WIN_X64]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-win-x64.zip`,
[WIN_ARM]: `https://nodejs.org/download/release/${NODE_VERSION}/node-${NODE_VERSION}-win-arm64.zip`,
};
const SRC_BIN_MAP = {
[MAC_ARM]: `node-${NODE_VERSION}-darwin-arm64/bin/node`,
[MAC_X64]: `node-${NODE_VERSION}-darwin-x64/bin/node`,
[LNX_ARM]: `node-${NODE_VERSION}-linux-arm64/bin/node`,
[LNX_X64]: `node-${NODE_VERSION}-linux-x64/bin/node`,
[WIN_X64]: `node-${NODE_VERSION}-win-x64/node.exe`,
[WIN_ARM]: `node-${NODE_VERSION}-win-arm64/node.exe`,
};
const DST_BIN_MAP = {
[MAC_ARM]: "yaaknode",
[MAC_X64]: "yaaknode",
[LNX_ARM]: "yaaknode",
[LNX_X64]: "yaaknode",
[WIN_X64]: "yaaknode.exe",
[WIN_ARM]: "yaaknode.exe",
};
const SHA256_MAP = {
[MAC_ARM]: "b05aa3a66efe680023f930bd5af3fdbbd542794da5644ca2ad711d68cbd4dc35",
[MAC_X64]: "096081b6d6fcdd3f5ba0f5f1d44a47e83037ad2e78eada26671c252fe64dd111",
[LNX_ARM]: "0dc93ec5c798b0d347f068db6d205d03dea9a71765e6a53922b682b91265d71f",
[LNX_X64]: "58a5ff5cc8f2200e458bea22e329d5c1994aa1b111d499ca46ec2411d58239ca",
[WIN_X64]: "5355ae6d7c49eddcfde7d34ac3486820600a831bf81dc3bdca5c8db6a9bb0e76",
[WIN_ARM]: "ce9ee4e547ebdff355beb48e309b166c24df6be0291c9eaf103ce15f3de9e5b4",
};
const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`;
const destDir = path.join(__dirname, `..`, "crates-tauri", "yaak-app-client", "vendored", "node");
const binDest = path.join(destDir, DST_BIN_MAP[key]);
console.log(`Vendoring NodeJS ${NODE_VERSION} for ${key}`);
if (existsSync(binDest) && tryExecSync(`${binDest} --version`).trim() === NODE_VERSION) {
console.log("NodeJS already vendored");
return;
}
rmSync(destDir, { recursive: true, force: true });
mkdirSync(destDir, { recursive: true });
const url = URL_MAP[key];
const tmpDir = path.join(__dirname, "tmp-node");
rmSync(tmpDir, { recursive: true, force: true });
(async function () {
// Download GitHub release artifact
console.log("Downloading NodeJS at", url);
const { filePath } = await new Downloader({
url,
directory: tmpDir,
timeout: 1000 * 60 * 2,
}).download();
// Verify SHA256
const expectedHash = SHA256_MAP[key];
const fileBuffer = fs.readFileSync(filePath);
const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
if (actualHash !== expectedHash) {
throw new Error(
`SHA256 mismatch for ${path.basename(filePath)}\n expected: ${expectedHash}\n actual: ${actualHash}`,
);
}
console.log("SHA256 verified:", actualHash);
// Decompress to the same directory
await extractArchive(filePath, tmpDir);
// Copy binary
const binSrc = path.join(tmpDir, SRC_BIN_MAP[key]);
cpSync(binSrc, binDest);
rmSync(tmpDir, { recursive: true, force: true });
console.log("Downloaded NodeJS to", binDest);
})().catch((err) => {
console.log("Script failed:", err);
process.exit(1);
});
function tryExecSync(cmd) {
try {
return execSync(cmd, { stdio: "pipe" }).toString("utf-8");
} catch {
return "";
}
}

View File

@@ -0,0 +1,41 @@
const { readdirSync, cpSync, existsSync, mkdirSync } = require("node:fs");
const path = require("node:path");
const pluginsDir = path.join(__dirname, "..", "plugins");
const externalPluginsDir = path.join(__dirname, "..", "plugins-external");
// Get list of external (non-bundled) plugins
const externalPlugins = new Set();
if (existsSync(externalPluginsDir)) {
for (const name of readdirSync(externalPluginsDir)) {
if (!name.startsWith(".")) {
externalPlugins.add(name);
}
}
}
console.log("Copying Yaak plugins to", pluginsDir);
for (const name of readdirSync(pluginsDir)) {
const dir = path.join(pluginsDir, name);
if (name.startsWith(".")) continue;
if (externalPlugins.has(name)) {
console.log(`Skipping ${name} (external plugin)`);
continue;
}
const destDir = path.join(
__dirname,
"..",
"crates-tauri",
"yaak-app-client",
"vendored",
"plugins",
name,
);
mkdirSync(destDir, { recursive: true });
console.log(`Copying ${name} to ${destDir}`);
cpSync(path.join(dir, "package.json"), path.join(destDir, "package.json"));
cpSync(path.join(dir, "build"), path.join(destDir, "build"), {
recursive: true,
});
}

114
scripts/vendor-protoc.cjs Normal file
View File

@@ -0,0 +1,114 @@
const crypto = require("node:crypto");
const fs = require("node:fs");
const Downloader = require("nodejs-file-downloader");
const { extractArchive } = require("./extract-archive.cjs");
const path = require("node:path");
const { rmSync, mkdirSync, cpSync, existsSync, statSync, chmodSync } = require("node:fs");
const { execSync } = require("node:child_process");
const VERSION = "33.1";
// `${process.platform}_${process.arch}`
const MAC_ARM = "darwin_arm64";
const MAC_X64 = "darwin_x64";
const LNX_ARM = "linux_arm64";
const LNX_X64 = "linux_x64";
const WIN_X64 = "win32_x64";
const WIN_ARM = "win32_arm64";
const URL_MAP = {
[MAC_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-aarch_64.zip`,
[MAC_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-osx-x86_64.zip`,
[LNX_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-aarch_64.zip`,
[LNX_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-x86_64.zip`,
[WIN_X64]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-win64.zip`,
[WIN_ARM]: `https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-win64.zip`,
};
const SRC_BIN_MAP = {
[MAC_ARM]: "bin/protoc",
[MAC_X64]: "bin/protoc",
[LNX_ARM]: "bin/protoc",
[LNX_X64]: "bin/protoc",
[WIN_X64]: "bin/protoc.exe",
[WIN_ARM]: "bin/protoc.exe",
};
const DST_BIN_MAP = {
[MAC_ARM]: "yaakprotoc",
[MAC_X64]: "yaakprotoc",
[LNX_ARM]: "yaakprotoc",
[LNX_X64]: "yaakprotoc",
[WIN_X64]: "yaakprotoc.exe",
[WIN_ARM]: "yaakprotoc.exe",
};
const SHA256_MAP = {
[MAC_ARM]: "db7e66ff7f9080614d0f5505a6b0ac488cf89a15621b6a361672d1332ec2e14e",
[MAC_X64]: "e20b5f930e886da85e7402776a4959efb1ed60c57e72794bcade765e67abaa82",
[LNX_ARM]: "6018147740548e0e0f764408c87f4cd040e6e1c1203e13aeacaf811892b604f3",
[LNX_X64]: "f3340e28a83d1c637d8bafdeed92b9f7db6a384c26bca880a6e5217b40a4328b",
[WIN_X64]: "d7a207fb6eec0e4b1b6613be3b7d11905375b6fd1147a071116eb8e9f24ac53b",
[WIN_ARM]: "d7a207fb6eec0e4b1b6613be3b7d11905375b6fd1147a071116eb8e9f24ac53b",
};
const dstDir = path.join(__dirname, `..`, "crates-tauri", "yaak-app-client", "vendored", "protoc");
const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`;
console.log(`Vendoring protoc ${VERSION} for ${key}`);
const url = URL_MAP[key];
const tmpDir = path.join(__dirname, "tmp-protoc");
const binSrc = path.join(tmpDir, SRC_BIN_MAP[key]);
const binDst = path.join(dstDir, DST_BIN_MAP[key]);
if (existsSync(binDst) && tryExecSync(`${binDst} --version`).trim().includes(VERSION)) {
console.log("Protoc already vendored");
return;
}
rmSync(tmpDir, { recursive: true, force: true });
rmSync(dstDir, { recursive: true, force: true });
mkdirSync(dstDir, { recursive: true });
(async function () {
// Download GitHub release artifact
const { filePath } = await new Downloader({ url, directory: tmpDir }).download();
// Verify SHA256
const expectedHash = SHA256_MAP[key];
const fileBuffer = fs.readFileSync(filePath);
const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
if (actualHash !== expectedHash) {
throw new Error(
`SHA256 mismatch for ${path.basename(filePath)}\n expected: ${expectedHash}\n actual: ${actualHash}`,
);
}
console.log("SHA256 verified:", actualHash);
// Decompress to the same directory
await extractArchive(filePath, tmpDir);
// Copy binary
cpSync(binSrc, binDst);
// Copy other files
const includeSrc = path.join(tmpDir, "include");
const includeDst = path.join(dstDir, "include");
cpSync(includeSrc, includeDst, { recursive: true });
rmSync(tmpDir, { recursive: true, force: true });
// Make binary writable, so we can sign it during release
const stat = statSync(binDst);
const newMode = stat.mode | 0o200;
chmodSync(binDst, newMode);
console.log("Downloaded protoc to", binDst);
})().catch((err) => console.log("Script failed:", err));
function tryExecSync(cmd) {
try {
return execSync(cmd, { stdio: "pipe" }).toString("utf-8");
} catch {
return "";
}
}