release: 2026-08-01

This commit is contained in:
moaljumaa
2026-08-01 14:01:17 +02:00
commit 7d8b930bc3
137 changed files with 34695 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env node
// Force a run's next step to fire immediately instead of waiting for the
// normal delay/recheck schedule. Useful when testing a workflow end-to-end
// on a single lead, without waiting hours for the real schedule.
//
// Usage:
// node scripts/force-next-step.js --run <run_id> --target <target_id> [--apply] [--skip-schedule]
// node scripts/force-next-step.js --run <run_id> --all-targets [--apply] [--skip-schedule] (whole run — careful, real sends)
// node scripts/force-next-step.js --list (list active runs)
//
// Without --apply this only PRINTS what would change (dry run). Pass --apply
// to actually update the DB. DB path follows the same LINKI_DB_PATH env var
// (or ./linki.db) that the app itself uses.
//
// --target is required unless you pass --all-targets: forcing an entire real
// campaign to fire at once means every eligible lead gets messaged/connected
// right now instead of on its normal schedule. Scope to one lead for testing.
//
// --skip-schedule: setting next_step_at alone isn't enough — the runner
// re-checks the account's working-hours window (active_hours_start/end,
// timezone, working_days) on every tick and reschedules again if outside it
// ("Outside working schedule"). This flag additionally widens the run's
// LinkedIn account (and email account, if any) to a 24/7 window so the
// forced step actually fires on the next tick instead of bouncing. Only
// takes effect together with --apply. Does NOT auto-restore the original
// hours — note the printed "was" values and set them back in Settings when
// you're done testing.
const path = require("path");
const Database = require("better-sqlite3");
const DB_PATH = process.env.LINKI_DB_PATH || path.join(process.cwd(), "linki.db");
function parseArgs(argv) {
const args = { apply: false, list: false, allTargets: false, skipSchedule: false, run: null, target: null };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--apply") args.apply = true;
else if (a === "--list") args.list = true;
else if (a === "--all-targets") args.allTargets = true;
else if (a === "--skip-schedule") args.skipSchedule = true;
else if (a === "--run") args.run = argv[++i];
else if (a === "--target") args.target = argv[++i];
}
return args;
}
function widenSchedule(db, table, id) {
const before = db.prepare(`SELECT active_hours_start, active_hours_end, working_days FROM ${table} WHERE id = ?`).get(id);
if (!before) return null;
db.prepare(`UPDATE ${table} SET active_hours_start = 0, active_hours_end = 24, working_days = '1,2,3,4,5,6,7' WHERE id = ?`).run(id);
return before;
}
function main() {
const args = parseArgs(process.argv.slice(2));
const db = new Database(DB_PATH);
if (args.list) {
const runs = db
.prepare(
`SELECT r.id, r.status, w.name as workflow_name, r.created_at
FROM runs r JOIN workflows w ON w.id = r.workflow_id
WHERE r.status IN ('running', 'paused')
ORDER BY r.created_at DESC`
)
.all();
console.log(JSON.stringify(runs, null, 2));
db.close();
return;
}
if (!args.run) {
console.error("Usage: node scripts/force-next-step.js --run <run_id> --target <target_id> [--apply]");
console.error(" node scripts/force-next-step.js --run <run_id> --all-targets [--apply]");
console.error(" node scripts/force-next-step.js --list");
process.exit(1);
}
if (!args.target && !args.allTargets) {
console.error("Refusing to run: pass --target <target_id> to scope this to one lead (recommended for testing),");
console.error("or --all-targets to force the ENTIRE run (every eligible lead fires immediately — real sends).");
db.close();
process.exit(1);
}
const where = ["rp.run_id = ?"];
const params = [args.run];
if (args.target) {
where.push("rp.target_id = ?");
params.push(args.target);
}
const tracks = db
.prepare(
`SELECT rt.id, rt.track, rt.state, rt.current_step, rt.next_step_at,
rp.target_id, t.full_name
FROM run_profile_tracks rt
JOIN run_profiles rp ON rp.id = rt.run_profile_id
JOIN targets t ON t.id = rp.target_id
WHERE ${where.join(" AND ")} AND rt.state != 'completed'`
)
.all(...params);
if (tracks.length === 0) {
console.log("No eligible tracks found for that run/target.");
db.close();
return;
}
console.log(`${args.apply ? "Updating" : "[DRY RUN] Would update"} ${tracks.length} track(s):`);
for (const t of tracks) {
console.log(` - ${t.full_name} (${t.track}, step ${t.current_step}, state=${t.state}) next_step_at: ${t.next_step_at} -> now`);
}
if (args.skipSchedule) {
const run = db.prepare("SELECT account_id, email_account_id FROM runs WHERE id = ?").get(args.run);
if (!run) {
console.error(`No run found with id ${args.run}`);
db.close();
process.exit(1);
}
if (!args.apply) {
console.log("\n[DRY RUN] --skip-schedule would widen this run's account(s) to a 24/7 window.");
} else {
const accountBefore = widenSchedule(db, "accounts", run.account_id);
if (accountBefore) {
console.log(`Widened LinkedIn account ${run.account_id} to 24/7 (was: ${accountBefore.active_hours_start}-${accountBefore.active_hours_end}, days ${accountBefore.working_days}). Restore this in Settings when done testing.`);
}
if (run.email_account_id) {
const emailBefore = widenSchedule(db, "email_accounts", run.email_account_id);
if (emailBefore) {
console.log(`Widened email account ${run.email_account_id} to 24/7 (was: ${emailBefore.active_hours_start}-${emailBefore.active_hours_end}, days ${emailBefore.working_days}). Restore this in Settings when done testing.`);
}
}
}
}
if (args.apply) {
const update = db.prepare("UPDATE run_profile_tracks SET next_step_at = datetime('now') WHERE id = ?");
const tx = db.transaction((rows) => {
for (const t of rows) update.run(t.id);
});
tx(tracks);
console.log("Done. The runner will pick these up on its next loop tick (a few seconds).");
} else {
console.log("\nDry run only — pass --apply to actually update the DB.");
}
db.close();
}
main();
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/*
* scripts/mirror-ee.js — runs at predev/prebuild.
*
* Next.js Pages Router only routes files physically inside pages/. Premium pages/APIs
* live in ee/pages/** so they can be stripped from the public build, but they must be
* COPIED into pages/** for Next to see them during dev/build in the private repo.
*
* This script copies ee/pages/** → pages/** and records every file it wrote in
* pages/.ee-mirror-manifest.json. It also writes a generated pages/.gitignore listing
* every mirrored path — git reads nested .gitignore files natively, so the copies are
* never tracked in either repo and pages/ stays clean in git. Source of truth is ee/pages.
*
* When ee/ is absent (public build), this is a no-op. See docs/OPEN_CORE.md.
*
* Everything else in ee/ (lib code) is imported in place via @/ee — NOT mirrored.
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.resolve(__dirname, "..");
const EE_PAGES = path.join(ROOT, "ee", "pages");
const PAGES = path.join(ROOT, "pages");
const MANIFEST = path.join(PAGES, ".ee-mirror-manifest.json");
const NESTED_IGNORE = path.join(PAGES, ".gitignore");
function cleanPrevious() {
// Remove files written by a previous mirror run so renames/deletions in ee/pages
// don't leave stale routes behind.
if (!fs.existsSync(MANIFEST)) return;
try {
const prev = JSON.parse(fs.readFileSync(MANIFEST, "utf8"));
for (const rel of prev.files ?? []) {
const p = path.join(PAGES, rel);
if (fs.existsSync(p)) fs.rmSync(p);
}
fs.rmSync(MANIFEST);
if (fs.existsSync(NESTED_IGNORE)) fs.rmSync(NESTED_IGNORE);
} catch {
/* ignore a corrupt manifest — fresh mirror below overwrites cleanly */
}
}
function walk(dir, base = dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...walk(abs, base));
else out.push(path.relative(base, abs));
}
return out;
}
function main() {
cleanPrevious();
if (!fs.existsSync(EE_PAGES)) {
console.log("[mirror-ee] no ee/pages (public build or none yet) — nothing to mirror");
return;
}
const files = walk(EE_PAGES);
for (const rel of files) {
const src = path.join(EE_PAGES, rel);
const dst = path.join(PAGES, rel);
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.copyFileSync(src, dst);
}
fs.writeFileSync(
MANIFEST,
JSON.stringify({ generatedAt: new Date().toISOString(), files }, null, 2)
);
// Nested .gitignore so the mirrored copies are never tracked. Git reads this natively.
// Paths are POSIX-style and relative to pages/. The manifest + this file ignore themselves.
// IMPORTANT: gitignore treats [ ] * ? as glob metacharacters, so Next's dynamic-route
// segments like [replyId] must be backslash-escaped or the pattern won't match the literal
// directory (leaving the mirrored copy tracked). Escape them.
const esc = (p) => p.replace(/([[\]*?])/g, "\\$1");
const ignoreBody = [
"# GENERATED by scripts/mirror-ee.js — do not edit. Lists premium pages mirrored",
"# from ee/pages/. Source of truth is ee/pages/**. See docs/OPEN_CORE.md.",
".gitignore",
".ee-mirror-manifest.json",
...files.map((f) => "/" + esc(f.split(path.sep).join("/"))),
"",
].join("\n");
fs.writeFileSync(NESTED_IGNORE, ignoreBody);
console.log(`[mirror-ee] mirrored ${files.length} premium page(s) into pages/`);
}
main();
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/*
* scripts/strip-mirrored-pages.js <targetDir>
*
* Removes the premium pages that scripts/mirror-ee.js copied into pages/**, using the
* manifest it wrote. Also removes the generated pages/.gitignore and prunes any now-empty
* directories left behind (e.g. pages/api/oauth/ after all its routes are stripped).
*
* Used by .publish/publish.sh and .publish/dry-run.sh so the public tree contains only
* genuinely open-core routes. Idempotent; no-op if there's no manifest. See docs/OPEN_CORE.md.
*/
const fs = require("fs");
const path = require("path");
const root = process.argv[2] || process.cwd();
const pagesDir = path.join(root, "pages");
const manifestPath = path.join(pagesDir, ".ee-mirror-manifest.json");
if (!fs.existsSync(manifestPath)) {
console.log(" (no mirror manifest — nothing to strip)");
process.exit(0);
}
const { files } = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
// Remove mirrored files, collecting their parent dirs for empty-pruning.
const touchedDirs = new Set();
for (const rel of files) {
const p = path.join(pagesDir, rel);
if (fs.existsSync(p)) fs.rmSync(p);
touchedDirs.add(path.dirname(p));
}
fs.rmSync(manifestPath);
const gi = path.join(pagesDir, ".gitignore");
if (fs.existsSync(gi)) fs.rmSync(gi);
// Prune empty dirs bottom-up (deepest first), but never remove pages/ itself.
const dirsDeepestFirst = [...touchedDirs].sort((a, b) => b.length - a.length);
for (const dir of dirsDeepestFirst) {
let d = dir;
while (d.startsWith(pagesDir) && d !== pagesDir) {
if (fs.existsSync(d) && fs.readdirSync(d).length === 0) {
fs.rmdirSync(d);
d = path.dirname(d);
} else {
break;
}
}
}
console.log(` stripped ${files.length} mirrored premium page(s)`);