neuralos 3.2.16 → 3.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/gybackend.cjs +518 -250
  2. package/package.json +2 -2
package/bin/gybackend.cjs CHANGED
@@ -255452,6 +255452,256 @@ var require_pdf_parse2 = __commonJS({
255452
255452
  }
255453
255453
  });
255454
255454
 
255455
+ // ../../packages/backend/src/services/automation/templateEngine.ts
255456
+ var templateEngine_exports = {};
255457
+ __export(templateEngine_exports, {
255458
+ diffStrings: () => diffStrings,
255459
+ renderTemplate: () => renderTemplate2
255460
+ });
255461
+ function resolve(expr, scope) {
255462
+ const parts = expr.trim().split(".");
255463
+ let cur = scope;
255464
+ for (const p of parts) {
255465
+ if (cur == null) return void 0;
255466
+ if (typeof cur === "object") {
255467
+ cur = cur[p];
255468
+ } else {
255469
+ return void 0;
255470
+ }
255471
+ }
255472
+ return cur;
255473
+ }
255474
+ function applyFilters(value, filters, scope) {
255475
+ let v = value;
255476
+ for (const f of filters) {
255477
+ const m2 = f.match(/^(\w+)\s*(?:\((.*)\))?$/);
255478
+ if (!m2) continue;
255479
+ const name = m2[1];
255480
+ const argRaw = m2[2] ?? "";
255481
+ const arg = argRaw.replace(/^['"]|['"]$/g, "");
255482
+ switch (name) {
255483
+ case "default":
255484
+ if (v === void 0 || v === null || v === "") {
255485
+ v = arg in scope ? resolve(arg, scope) : arg;
255486
+ }
255487
+ break;
255488
+ case "upper":
255489
+ v = String(v ?? "").toUpperCase();
255490
+ break;
255491
+ case "lower":
255492
+ v = String(v ?? "").toLowerCase();
255493
+ break;
255494
+ case "length":
255495
+ if (Array.isArray(v)) v = v.length;
255496
+ else if (typeof v === "string") v = v.length;
255497
+ else v = 0;
255498
+ break;
255499
+ default:
255500
+ break;
255501
+ }
255502
+ }
255503
+ return v;
255504
+ }
255505
+ function evalExpr(expr, scope) {
255506
+ const parts = expr.split("|").map((p) => p.trim());
255507
+ const varExpr = parts[0];
255508
+ const filters = parts.slice(1);
255509
+ let val = resolve(varExpr, scope);
255510
+ val = applyFilters(val, filters, scope);
255511
+ if (val === void 0 || val === null) return "";
255512
+ if (Array.isArray(val)) return val.join(", ");
255513
+ if (typeof val === "object") return JSON.stringify(val);
255514
+ return String(val);
255515
+ }
255516
+ function evalCond(expr, scope) {
255517
+ const e = expr.trim();
255518
+ const eq = e.match(/^(.+?)\s*(==|!=)\s*(.+)$/);
255519
+ if (eq) {
255520
+ const left = resolve(eq[1].trim(), scope);
255521
+ const rightRaw = eq[3].trim();
255522
+ let right;
255523
+ if (/^['"].*['"]$/.test(rightRaw)) right = rightRaw.replace(/^['"]|['"]$/g, "");
255524
+ else if (/^-?\d+$/.test(rightRaw)) right = parseInt(rightRaw, 10);
255525
+ else right = resolve(rightRaw, scope);
255526
+ if (eq[2] === "==") return left == right;
255527
+ return left != right;
255528
+ }
255529
+ const v = resolve(e, scope);
255530
+ if (Array.isArray(v)) return v.length > 0;
255531
+ if (typeof v === "string") return v.length > 0;
255532
+ return Boolean(v);
255533
+ }
255534
+ function cleanTag(inner) {
255535
+ return inner.replace(/^-|\s-$/g, "").trim();
255536
+ }
255537
+ function tokenize3(tpl) {
255538
+ const tokenRe = /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\})/g;
255539
+ let lastIndex = 0;
255540
+ const tokens = [];
255541
+ let m2;
255542
+ while ((m2 = tokenRe.exec(tpl)) !== null) {
255543
+ if (m2.index > lastIndex) tokens.push({ type: "text", value: tpl.slice(lastIndex, m2.index) });
255544
+ const tok = m2[0];
255545
+ if (tok.startsWith("{%")) tokens.push({ type: "tag", value: cleanTag(tok.slice(2, -2)) });
255546
+ else tokens.push({ type: "expr", value: tok.slice(2, -2).trim() });
255547
+ lastIndex = m2.index + tok.length;
255548
+ }
255549
+ if (lastIndex < tpl.length) tokens.push({ type: "text", value: tpl.slice(lastIndex) });
255550
+ return tokens;
255551
+ }
255552
+ function findMatchingEnd(toks, start, open, close) {
255553
+ let depth = 1;
255554
+ for (let j = start + 1; j < toks.length; j++) {
255555
+ if (toks[j].type !== "tag") continue;
255556
+ const f = toks[j].value.split(/\s+/)[0];
255557
+ if (f === open) depth++;
255558
+ else if (f === close) {
255559
+ depth--;
255560
+ if (depth === 0) return j;
255561
+ }
255562
+ }
255563
+ throw new Error(`Unbalanced {% ${open} %}: missing {% ${close} %}`);
255564
+ }
255565
+ function findMatchingEndIf(toks, start) {
255566
+ let depth = 1;
255567
+ for (let j = start + 1; j < toks.length; j++) {
255568
+ if (toks[j].type !== "tag") continue;
255569
+ const f = toks[j].value.split(/\s+/)[0];
255570
+ if (f === "if") depth++;
255571
+ else if (f === "endif") {
255572
+ depth--;
255573
+ if (depth === 0) return j;
255574
+ }
255575
+ }
255576
+ throw new Error("Unbalanced {% if %}: missing {% endif %}");
255577
+ }
255578
+ function splitIfElse(body) {
255579
+ const ifBody = [];
255580
+ const elseBody = [];
255581
+ const elifs = [];
255582
+ let cur = ifBody;
255583
+ let curElif = null;
255584
+ let depth = 0;
255585
+ for (const t of body) {
255586
+ if (t.type === "tag") {
255587
+ const f = t.value.split(/\s+/)[0];
255588
+ if (f === "if") depth++;
255589
+ if (f === "endif") depth = Math.max(0, depth - 1);
255590
+ if (depth === 0 && f === "elif") {
255591
+ const cond = t.value.replace(/^elif\s+/, "");
255592
+ curElif = { cond, body: [] };
255593
+ elifs.push(curElif);
255594
+ cur = curElif.body;
255595
+ continue;
255596
+ }
255597
+ if (depth === 0 && f === "else") {
255598
+ cur = elseBody;
255599
+ continue;
255600
+ }
255601
+ }
255602
+ cur.push(t);
255603
+ }
255604
+ return { ifBody, elseBody, elifs };
255605
+ }
255606
+ function renderTemplate2(tpl, vars = {}) {
255607
+ const tokens = tokenize3(tpl);
255608
+ function renderTokens2(toks, scope) {
255609
+ let s = "";
255610
+ let idx = 0;
255611
+ while (idx < toks.length) {
255612
+ const t = toks[idx];
255613
+ if (t.type === "text") {
255614
+ s += t.value;
255615
+ idx++;
255616
+ continue;
255617
+ }
255618
+ if (t.type === "expr") {
255619
+ s += evalExpr(t.value, scope);
255620
+ idx++;
255621
+ continue;
255622
+ }
255623
+ const tag = t.value;
255624
+ const first = tag.split(/\s+/)[0];
255625
+ if (first === "for") {
255626
+ const fm = tag.match(/^for\s+(\w+)\s+in\s+(.+)$/);
255627
+ const fk = tag.match(/^for\s+(\w+)\s*,\s*(\w+)\s+in\s+(.+?)\.items\(\)$/);
255628
+ const bodyEnd = findMatchingEnd(toks, idx, "for", "endfor");
255629
+ const body = toks.slice(idx + 1, bodyEnd);
255630
+ idx = bodyEnd + 1;
255631
+ if (fk) {
255632
+ const kName = fk[1];
255633
+ const vName = fk[2];
255634
+ const obj = resolve(fk[3], scope);
255635
+ if (obj && typeof obj === "object") {
255636
+ for (const [k, v] of Object.entries(obj)) {
255637
+ s += renderTokens2(body, { ...scope, [kName]: k, [vName]: v });
255638
+ }
255639
+ }
255640
+ } else if (fm) {
255641
+ const name = fm[1];
255642
+ const list = resolve(fm[2], scope);
255643
+ if (Array.isArray(list)) {
255644
+ for (const item of list) s += renderTokens2(body, { ...scope, [name]: item });
255645
+ } else if (typeof list === "string") {
255646
+ for (const ch of list) s += renderTokens2(body, { ...scope, [name]: ch });
255647
+ } else if (list && typeof list === "object") {
255648
+ for (const [k, v] of Object.entries(list)) {
255649
+ s += renderTokens2(body, { ...scope, [name]: { key: k, value: v } });
255650
+ }
255651
+ }
255652
+ }
255653
+ continue;
255654
+ }
255655
+ if (first === "if") {
255656
+ const cond = tag.replace(/^if\s+/, "");
255657
+ const bodyEnd = findMatchingEndIf(toks, idx);
255658
+ const { ifBody, elseBody, elifs } = splitIfElse(toks.slice(idx + 1, bodyEnd));
255659
+ idx = bodyEnd + 1;
255660
+ let rendered = false;
255661
+ if (evalCond(cond, scope)) {
255662
+ s += renderTokens2(ifBody, scope);
255663
+ rendered = true;
255664
+ } else {
255665
+ for (const e of elifs) {
255666
+ if (evalCond(e.cond, scope)) {
255667
+ s += renderTokens2(e.body, scope);
255668
+ rendered = true;
255669
+ break;
255670
+ }
255671
+ }
255672
+ if (!rendered && elseBody.length) s += renderTokens2(elseBody, scope);
255673
+ }
255674
+ continue;
255675
+ }
255676
+ idx++;
255677
+ }
255678
+ return s;
255679
+ }
255680
+ return renderTokens2(tokens, vars);
255681
+ }
255682
+ function diffStrings(a, b) {
255683
+ const aLines = a.split("\n");
255684
+ const bLines = b.split("\n");
255685
+ const lines = [];
255686
+ const max = Math.max(aLines.length, bLines.length);
255687
+ for (let i = 0; i < max; i++) {
255688
+ const al = aLines[i];
255689
+ const bl = bLines[i];
255690
+ if (al === bl) {
255691
+ if (al !== void 0) lines.push(` ${al}`);
255692
+ } else {
255693
+ if (al !== void 0) lines.push(`- ${al}`);
255694
+ if (bl !== void 0) lines.push(`+ ${bl}`);
255695
+ }
255696
+ }
255697
+ return lines.join("\n");
255698
+ }
255699
+ var init_templateEngine = __esm({
255700
+ "../../packages/backend/src/services/automation/templateEngine.ts"() {
255701
+ "use strict";
255702
+ }
255703
+ });
255704
+
255455
255705
  // ../../packages/backend/src/services/automation/schedulerService.ts
255456
255706
  var schedulerService_exports = {};
255457
255707
  __export(schedulerService_exports, {
@@ -256465,14 +256715,18 @@ var init_scheduledTaskRunner = __esm({
256465
256715
  // ../../packages/backend/src/services/automation/playbookRunner.ts
256466
256716
  var playbookRunner_exports = {};
256467
256717
  __export(playbookRunner_exports, {
256718
+ buildEnvPrefix: () => buildEnvPrefix,
256719
+ buildPlaybookDryRunPlan: () => buildPlaybookDryRunPlan,
256468
256720
  clearPlaybookRuns: () => clearPlaybookRuns,
256721
+ evaluateWhen: () => evaluateWhen,
256469
256722
  executePlaybook: () => executePlaybook,
256470
256723
  getPlaybookRun: () => getPlaybookRun,
256471
256724
  listPlaybookRuns: () => listPlaybookRuns,
256472
256725
  matchExpectation: () => matchExpectation,
256473
256726
  resolvePlaybookStepCommand: () => resolvePlaybookStepCommand,
256474
256727
  resolveRollbackCommand: () => resolveRollbackCommand,
256475
- resolveValidationCommand: () => resolveValidationCommand
256728
+ resolveValidationCommand: () => resolveValidationCommand,
256729
+ runCommandWithTimeout: () => runCommandWithTimeout
256476
256730
  });
256477
256731
  function listPlaybookRuns() {
256478
256732
  return runHistory;
@@ -256485,6 +256739,9 @@ function clearPlaybookRuns() {
256485
256739
  }
256486
256740
  function resolvePlaybookStepCommand(step, automationManager) {
256487
256741
  if (step.kind === "wait") return null;
256742
+ if (step.kind === "template") return null;
256743
+ if (step.kind === "playbook") return null;
256744
+ if (step.kind === "healthCheck") return null;
256488
256745
  if (step.kind === "command") {
256489
256746
  const cmd2 = (step.command ?? "").trim();
256490
256747
  if (!cmd2) throw new Error(`Step "${step.name ?? step.id}": empty command`);
@@ -256498,6 +256755,96 @@ function resolvePlaybookStepCommand(step, automationManager) {
256498
256755
  if (!cmd) throw new Error(`Step "${step.name ?? step.id}" script "${script.name}" has an empty command`);
256499
256756
  return cmd;
256500
256757
  }
256758
+ async function runCommandWithTimeout(deps, terminalId, command, timeoutSeconds) {
256759
+ if (!timeoutSeconds || timeoutSeconds <= 0) {
256760
+ const r = await deps.terminalService.runCommandAndWait(terminalId, command);
256761
+ return { exitCode: r.exitCode, stdoutDelta: r.stdoutDelta ?? "", timedOut: false };
256762
+ }
256763
+ return await new Promise((resolve2) => {
256764
+ let settled = false;
256765
+ const settle = (v) => {
256766
+ if (settled) return;
256767
+ settled = true;
256768
+ clearTimeout(timer);
256769
+ resolve2(v);
256770
+ };
256771
+ const timer = setTimeout(() => {
256772
+ settle({ exitCode: -1, stdoutDelta: "", timedOut: true });
256773
+ }, timeoutSeconds * 1e3);
256774
+ deps.terminalService.runCommandAndWait(terminalId, command).then((r) => settle({ exitCode: r.exitCode, stdoutDelta: r.stdoutDelta ?? "", timedOut: false })).catch(() => settle({ exitCode: -1, stdoutDelta: "", timedOut: false }));
256775
+ });
256776
+ }
256777
+ function evaluateWhen(when, paramValues) {
256778
+ const exprMatch = when.match(/^\{\{\s*([\w.]+)\s*\}\}\s*(==|!=)\s*(.+)$/);
256779
+ if (exprMatch) {
256780
+ const [, varName, op, rawValue2] = exprMatch;
256781
+ const actual = paramValues[varName] ?? "";
256782
+ const expected = rawValue2.trim().replace(/^['"]|['"]$/g, "");
256783
+ return { mode: "expression", result: op === "==" ? actual === expected : actual !== expected };
256784
+ }
256785
+ return { mode: "command", result: false, command: when };
256786
+ }
256787
+ function buildEnvPrefix(env, paramValues) {
256788
+ if (!env || Object.keys(env).length === 0) return "";
256789
+ const exports2 = Object.entries(env).map(([k, v]) => {
256790
+ const value = v.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, name) => paramValues[name] ?? "");
256791
+ const safe = value.replace(/'/g, "'\\''");
256792
+ return `export ${k}='${safe}'`;
256793
+ });
256794
+ return exports2.join(" && ") + " && ";
256795
+ }
256796
+ function buildPlaybookDryRunPlan(deps, playbook) {
256797
+ const settings = deps.getSettings();
256798
+ const scope = { groupId: playbook.groupId, tags: playbook.tags, targets: playbook.targets };
256799
+ const targets = resolveScheduledTaskTargets(scope, settings);
256800
+ const notes = [];
256801
+ const steps = [];
256802
+ playbook.steps.forEach((step, i) => {
256803
+ const entry = {
256804
+ index: i,
256805
+ name: step.name,
256806
+ kind: step.kind,
256807
+ timeoutSeconds: step.timeoutSeconds ?? playbook.defaultStepTimeoutSeconds,
256808
+ retryAttempts: step.retryAttempts ?? playbook.defaultStepRetryAttempts,
256809
+ when: step.when
256810
+ };
256811
+ try {
256812
+ if (step.kind === "wait") {
256813
+ entry.command = `# wait ${step.waitSeconds ?? 0}s`;
256814
+ } else if (step.kind === "template") {
256815
+ entry.command = `# render template ${step.templateId} \u2192 ${step.templateTargetPath ?? "(no path)"}`;
256816
+ if (!step.templateId) notes.push(`Step ${i + 1}: template step missing templateId`);
256817
+ if (!step.templateTargetPath) notes.push(`Step ${i + 1}: template step missing templateTargetPath`);
256818
+ } else if (step.kind === "playbook") {
256819
+ entry.command = `# sub-playbook ${step.playbookId}`;
256820
+ if (!step.playbookId) notes.push(`Step ${i + 1}: playbook step missing playbookId`);
256821
+ } else if (step.kind === "healthCheck") {
256822
+ entry.command = `# healthCheck ${step.healthUrl} expect ${step.healthExpectStatus ?? 200}`;
256823
+ if (!step.healthUrl) notes.push(`Step ${i + 1}: healthCheck step missing healthUrl`);
256824
+ } else {
256825
+ const cmd = resolvePlaybookStepCommand(step, deps.automationManager);
256826
+ entry.command = cmd ?? void 0;
256827
+ }
256828
+ } catch (e) {
256829
+ entry.command = `# ERROR: ${e instanceof Error ? e.message : String(e)}`;
256830
+ notes.push(`Step ${i + 1}: ${e instanceof Error ? e.message : String(e)}`);
256831
+ }
256832
+ steps.push(entry);
256833
+ });
256834
+ if (playbook.maxParallelTargets && playbook.maxParallelTargets > 1) {
256835
+ notes.push(`Targets run in parallel (max ${playbook.maxParallelTargets}).`);
256836
+ }
256837
+ if (playbook.maxRuntimeMinutes) {
256838
+ notes.push(`Circuit breaker: aborts after ${playbook.maxRuntimeMinutes} minutes.`);
256839
+ }
256840
+ return {
256841
+ playbookId: playbook.id,
256842
+ playbookName: playbook.name,
256843
+ targets: targets.length === 0 ? [{ name: "local", kind: "local" }] : targets.map((t) => ({ name: t.name, kind: t.kind })),
256844
+ steps,
256845
+ notes
256846
+ };
256847
+ }
256501
256848
  function resolveValidationCommand(validate7, automationManager) {
256502
256849
  if ((validate7.command ?? "").trim()) return validate7.command.trim();
256503
256850
  const script = automationManager.listScripts().find((s) => s.id === validate7.scriptId);
@@ -256634,13 +256981,111 @@ async function executePlaybook(deps, playbook) {
256634
256981
  await sleep3(Math.max(0, (step.waitSeconds ?? 0) * 1e3));
256635
256982
  continue;
256636
256983
  }
256637
- const command = resolvePlaybookStepCommand(step, deps.automationManager);
256984
+ if (step.when) {
256985
+ const cond = evaluateWhen(step.when, {});
256986
+ if (cond.mode === "expression") {
256987
+ if (!cond.result) {
256988
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} skipped (when: ${step.when})`);
256989
+ stepOutcome.output = `# skipped (when: ${step.when})`;
256990
+ continue;
256991
+ }
256992
+ } else if (cond.command) {
256993
+ const check2 = await deps.terminalService.runCommandAndWait(terminalId, cond.command);
256994
+ if (check2.exitCode !== 0 && check2.exitCode !== void 0) {
256995
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} skipped (when-check exit ${check2.exitCode})`);
256996
+ stepOutcome.output = `# skipped (when-check exit ${check2.exitCode})`;
256997
+ continue;
256998
+ }
256999
+ }
257000
+ }
257001
+ if (step.kind === "template") {
257002
+ const template = deps.automationManager.listTemplates().find((t) => t.id === step.templateId);
257003
+ if (!template) throw new Error(`template step references missing template "${step.templateId}"`);
257004
+ if (!step.templateTargetPath) throw new Error("template step missing templateTargetPath");
257005
+ const { renderTemplate: renderTemplate3 } = await Promise.resolve().then(() => (init_templateEngine(), templateEngine_exports));
257006
+ const rendered = renderTemplate3(template.body, step.templateValues ?? {});
257007
+ const encoded = Buffer.from(rendered, "utf8").toString("base64");
257008
+ const writeCmd = `echo '${encoded}' | base64 -d > ${step.templateTargetPath}`;
257009
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} (template \u2192 ${step.templateTargetPath})`);
257010
+ const result2 = await runCommandWithTimeout(deps, terminalId, writeCmd, step.timeoutSeconds ?? playbook.defaultStepTimeoutSeconds);
257011
+ stepOutcome.exitCode = result2.exitCode;
257012
+ stepOutcome.output = `# wrote ${rendered.length} bytes to ${step.templateTargetPath}`;
257013
+ stepOutcome.ok = result2.exitCode === 0 || result2.exitCode === void 0;
257014
+ if (!stepOutcome.ok) stepOutcome.error = result2.timedOut ? "template write timed out" : `exit code ${result2.exitCode}`;
257015
+ recordLedger(name, stepOutcome, "execute", stepOutcome.ok, stepOutcome.output);
257016
+ if (!stepOutcome.ok) throw new Error(stepOutcome.error);
257017
+ continue;
257018
+ }
257019
+ if (step.kind === "healthCheck") {
257020
+ if (!step.healthUrl) throw new Error("healthCheck step missing healthUrl");
257021
+ const expectStatus = step.healthExpectStatus ?? 200;
257022
+ const timeoutSec2 = step.healthTimeoutSeconds ?? 60;
257023
+ const intervalSec = step.healthIntervalSeconds ?? 5;
257024
+ const deadline2 = Date.now() + timeoutSec2 * 1e3;
257025
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} (healthCheck ${step.healthUrl})`);
257026
+ let healthy = false;
257027
+ let lastStatus = 0;
257028
+ while (Date.now() < deadline2) {
257029
+ const checkCmd = `curl -s -o /dev/null -w '%{http_code}' ${step.healthUrl.replace(/'/g, "'\\''")}`;
257030
+ const r = await deps.terminalService.runCommandAndWait(terminalId, checkCmd);
257031
+ lastStatus = Number((r.stdoutDelta ?? "").trim()) || 0;
257032
+ if (lastStatus === expectStatus) {
257033
+ healthy = true;
257034
+ break;
257035
+ }
257036
+ await sleep3(intervalSec * 1e3);
257037
+ }
257038
+ stepOutcome.ok = healthy;
257039
+ stepOutcome.output = `# healthCheck ${step.healthUrl} \u2192 HTTP ${lastStatus} (expected ${expectStatus})`;
257040
+ if (!healthy) stepOutcome.error = `healthCheck failed: HTTP ${lastStatus} != ${expectStatus} after ${timeoutSec2}s`;
257041
+ recordLedger(name, stepOutcome, "execute", healthy, stepOutcome.output);
257042
+ if (!healthy) throw new Error(stepOutcome.error);
257043
+ continue;
257044
+ }
257045
+ if (step.kind === "playbook") {
257046
+ if (!step.playbookId) throw new Error("playbook step missing playbookId");
257047
+ const sub = deps.automationManager.listPlaybooks().find((p) => p.id === step.playbookId || p.name === step.playbookId);
257048
+ if (!sub) throw new Error(`playbook step references missing playbook "${step.playbookId}"`);
257049
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} (sub-playbook "${sub.name}")`);
257050
+ const subRecord = await executePlaybook(
257051
+ { ...deps, onLog: log },
257052
+ { ...sub, targets: playbook.targets, tags: playbook.tags, groupId: playbook.groupId }
257053
+ );
257054
+ stepOutcome.ok = subRecord.ok;
257055
+ stepOutcome.output = `# sub-playbook "${sub.name}" ${subRecord.ok ? "ok" : "FAILED"} on ${subRecord.targets.length} target(s)`;
257056
+ if (!subRecord.ok) {
257057
+ const failed = subRecord.targets.filter((t) => !t.ok).map((t) => t.target);
257058
+ stepOutcome.error = `sub-playbook "${sub.name}" failed on: ${failed.join(", ")}`;
257059
+ }
257060
+ recordLedger(name, stepOutcome, "execute", stepOutcome.ok, stepOutcome.output);
257061
+ if (!stepOutcome.ok) throw new Error(stepOutcome.error);
257062
+ continue;
257063
+ }
257064
+ const rawCommand = resolvePlaybookStepCommand(step, deps.automationManager);
257065
+ const command = buildEnvPrefix(step.env, {}) + rawCommand;
256638
257066
  log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1}/${playbook.steps.length}${step.name ? ` (${step.name})` : ""}`);
256639
- const result = await deps.terminalService.runCommandAndWait(terminalId, command);
256640
- stepOutcome.exitCode = result.exitCode;
256641
- stepOutcome.output = tail3(result.stdoutDelta ?? "");
256642
- stepOutcome.ok = result.exitCode === 0 || result.exitCode === void 0;
256643
- if (!stepOutcome.ok) stepOutcome.error = `exit code ${result.exitCode}`;
257067
+ const maxAttempts = 1 + Math.max(0, step.retryAttempts ?? playbook.defaultStepRetryAttempts ?? 0);
257068
+ const retryDelay = Math.max(0, step.retryDelaySeconds ?? 30);
257069
+ const timeoutSec = step.timeoutSeconds ?? playbook.defaultStepTimeoutSeconds;
257070
+ let attempt = 0;
257071
+ let result = null;
257072
+ while (attempt < maxAttempts) {
257073
+ attempt++;
257074
+ result = await runCommandWithTimeout(deps, terminalId, command, timeoutSec);
257075
+ const ok = result.exitCode === 0 || result.exitCode === void 0;
257076
+ if (ok) break;
257077
+ if (attempt < maxAttempts) {
257078
+ log(`[playbook] "${playbook.name}" @ ${name}: step ${i + 1} attempt ${attempt}/${maxAttempts} failed (${result.timedOut ? "timeout" : `exit ${result.exitCode}`}) \u2014 retrying in ${retryDelay}s`);
257079
+ await sleep3(retryDelay * 1e3);
257080
+ }
257081
+ }
257082
+ const finalResult = result;
257083
+ stepOutcome.exitCode = finalResult.exitCode;
257084
+ stepOutcome.output = step.outputCapture === "full" ? finalResult.stdoutDelta : tail3(finalResult.stdoutDelta);
257085
+ stepOutcome.ok = finalResult.exitCode === 0 || finalResult.exitCode === void 0;
257086
+ if (!stepOutcome.ok) {
257087
+ stepOutcome.error = finalResult.timedOut ? `timed out after ${timeoutSec}s` : `exit code ${finalResult.exitCode}`;
257088
+ }
256644
257089
  recordLedger(name, stepOutcome, "execute", stepOutcome.ok, stepOutcome.ok ? stepOutcome.output : stepOutcome.error);
256645
257090
  if (stepOutcome.ok && step.validate) {
256646
257091
  try {
@@ -256708,10 +257153,43 @@ async function executePlaybook(deps, playbook) {
256708
257153
  })
256709
257154
  );
256710
257155
  } else {
256711
- for (const target of targets) {
257156
+ const maxParallel = Math.max(1, playbook.maxParallelTargets ?? 1);
257157
+ const onTargetError = playbook.onTargetError ?? "stop";
257158
+ const runtimeDeadline = playbook.maxRuntimeMinutes ? Date.now() + playbook.maxRuntimeMinutes * 6e4 : Number.POSITIVE_INFINITY;
257159
+ const configs = targets.map((target) => {
256712
257160
  log(`[playbook] "${playbook.name}" \u2192 ${target.kind}://${target.name}`);
256713
257161
  const config2 = target.kind === "ssh" ? sshEntryToConfig(target.ssh, settings) : target.kind === "winrm" ? winrmEntryToConfig(target.winrm) : serialEntryToConfig(target.serial);
256714
- record2.targets.push(await runTarget(target.name, config2));
257162
+ return { name: target.name, config: config2 };
257163
+ });
257164
+ if (maxParallel <= 1) {
257165
+ for (const { name, config: config2 } of configs) {
257166
+ if (Date.now() > runtimeDeadline) {
257167
+ log(`[playbook] "${playbook.name}": maxRuntimeMinutes exceeded \u2014 aborting remaining targets`);
257168
+ record2.targets.push({ target: name, ok: false, steps: [], error: "playbook runtime limit exceeded" });
257169
+ if (onTargetError === "stop") break;
257170
+ continue;
257171
+ }
257172
+ const t = await runTarget(name, config2);
257173
+ record2.targets.push(t);
257174
+ if (!t.ok && onTargetError === "stop") {
257175
+ log(`[playbook] "${playbook.name}": target ${name} failed \u2014 stopping remaining targets (onTargetError=stop)`);
257176
+ break;
257177
+ }
257178
+ }
257179
+ } else {
257180
+ for (let i = 0; i < configs.length; i += maxParallel) {
257181
+ if (Date.now() > runtimeDeadline) {
257182
+ log(`[playbook] "${playbook.name}": maxRuntimeMinutes exceeded \u2014 aborting remaining targets`);
257183
+ break;
257184
+ }
257185
+ const batch = configs.slice(i, i + maxParallel);
257186
+ const results = await Promise.all(batch.map(({ name, config: config2 }) => runTarget(name, config2)));
257187
+ results.forEach((t) => record2.targets.push(t));
257188
+ if (onTargetError === "stop" && results.some((t) => !t.ok)) {
257189
+ log(`[playbook] "${playbook.name}": a target in this batch failed \u2014 stopping remaining targets (onTargetError=stop)`);
257190
+ break;
257191
+ }
257192
+ }
256715
257193
  }
256716
257194
  }
256717
257195
  record2.ok = record2.targets.every((t) => t.ok);
@@ -256910,6 +257388,34 @@ async function runPlaybook(args, context2) {
256910
257388
  emit6(context2, "run_playbook", args, msg2);
256911
257389
  return msg2;
256912
257390
  }
257391
+ if (args.dryRun) {
257392
+ const { buildPlaybookDryRunPlan: buildPlaybookDryRunPlan2 } = await Promise.resolve().then(() => (init_playbookRunner(), playbookRunner_exports));
257393
+ const plan = buildPlaybookDryRunPlan2(
257394
+ { automationManager: m2, getSettings: () => settings },
257395
+ playbook
257396
+ );
257397
+ const targetList = plan.targets.map((t) => `${t.kind}://${t.name}`).join(", ");
257398
+ const stepList = plan.steps.map((s) => {
257399
+ const bits = [
257400
+ s.command ?? "(no command)",
257401
+ s.timeoutSeconds ? `timeout=${s.timeoutSeconds}s` : "",
257402
+ s.retryAttempts ? `retry=${s.retryAttempts}` : "",
257403
+ s.when ? `when=${s.when}` : ""
257404
+ ].filter(Boolean).join(" ");
257405
+ return ` ${s.index + 1}. [${s.kind}]${s.name ? ` ${s.name}:` : ""} ${bits}`;
257406
+ }).join("\n");
257407
+ const notes = plan.notes.length ? `
257408
+ Notes:
257409
+ ${plan.notes.map((n2) => ` \u26A0 ${n2}`).join("\n")}` : "";
257410
+ const msg2 = `Dry-run plan for "${playbook.name}" (NOTHING EXECUTED):
257411
+
257412
+ Targets (${plan.targets.length}): ${targetList}
257413
+
257414
+ Steps (${plan.steps.length}):
257415
+ ${stepList}${notes}`;
257416
+ emit6(context2, "run_playbook", args, msg2);
257417
+ return msg2;
257418
+ }
256913
257419
  const settings = {
256914
257420
  connections: {
256915
257421
  ssh: context2.savedSshConnections ?? [],
@@ -257007,7 +257513,8 @@ var init_playbook_tools = __esm({
257007
257513
  runPlaybookSchema = external_exports.object({
257008
257514
  id: external_exports.string().optional().describe("Playbook id to run."),
257009
257515
  name: external_exports.string().optional().describe("Playbook name to run (when id is not given)."),
257010
- paramValues: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("Run-time values for the playbook's declared params (Advanced Automation).")
257516
+ paramValues: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("Run-time values for the playbook's declared params (Advanced Automation)."),
257517
+ dryRun: external_exports.boolean().optional().describe("v3.2.17: resolve targets + commands and return the plan WITHOUT executing anything. Use before a MOP change to preview what will happen.")
257011
257518
  });
257012
257519
  }
257013
257520
  });
@@ -352676,246 +353183,7 @@ ${truncateForFleet(banner, 1500)}
352676
353183
 
352677
353184
  // ../../packages/backend/src/services/AgentHelper/tools/automation_tools.ts
352678
353185
  init_zod();
352679
-
352680
- // ../../packages/backend/src/services/automation/templateEngine.ts
352681
- function resolve(expr, scope) {
352682
- const parts = expr.trim().split(".");
352683
- let cur = scope;
352684
- for (const p of parts) {
352685
- if (cur == null) return void 0;
352686
- if (typeof cur === "object") {
352687
- cur = cur[p];
352688
- } else {
352689
- return void 0;
352690
- }
352691
- }
352692
- return cur;
352693
- }
352694
- function applyFilters(value, filters, scope) {
352695
- let v = value;
352696
- for (const f of filters) {
352697
- const m2 = f.match(/^(\w+)\s*(?:\((.*)\))?$/);
352698
- if (!m2) continue;
352699
- const name = m2[1];
352700
- const argRaw = m2[2] ?? "";
352701
- const arg = argRaw.replace(/^['"]|['"]$/g, "");
352702
- switch (name) {
352703
- case "default":
352704
- if (v === void 0 || v === null || v === "") {
352705
- v = arg in scope ? resolve(arg, scope) : arg;
352706
- }
352707
- break;
352708
- case "upper":
352709
- v = String(v ?? "").toUpperCase();
352710
- break;
352711
- case "lower":
352712
- v = String(v ?? "").toLowerCase();
352713
- break;
352714
- case "length":
352715
- if (Array.isArray(v)) v = v.length;
352716
- else if (typeof v === "string") v = v.length;
352717
- else v = 0;
352718
- break;
352719
- default:
352720
- break;
352721
- }
352722
- }
352723
- return v;
352724
- }
352725
- function evalExpr(expr, scope) {
352726
- const parts = expr.split("|").map((p) => p.trim());
352727
- const varExpr = parts[0];
352728
- const filters = parts.slice(1);
352729
- let val = resolve(varExpr, scope);
352730
- val = applyFilters(val, filters, scope);
352731
- if (val === void 0 || val === null) return "";
352732
- if (Array.isArray(val)) return val.join(", ");
352733
- if (typeof val === "object") return JSON.stringify(val);
352734
- return String(val);
352735
- }
352736
- function evalCond(expr, scope) {
352737
- const e = expr.trim();
352738
- const eq = e.match(/^(.+?)\s*(==|!=)\s*(.+)$/);
352739
- if (eq) {
352740
- const left = resolve(eq[1].trim(), scope);
352741
- const rightRaw = eq[3].trim();
352742
- let right;
352743
- if (/^['"].*['"]$/.test(rightRaw)) right = rightRaw.replace(/^['"]|['"]$/g, "");
352744
- else if (/^-?\d+$/.test(rightRaw)) right = parseInt(rightRaw, 10);
352745
- else right = resolve(rightRaw, scope);
352746
- if (eq[2] === "==") return left == right;
352747
- return left != right;
352748
- }
352749
- const v = resolve(e, scope);
352750
- if (Array.isArray(v)) return v.length > 0;
352751
- if (typeof v === "string") return v.length > 0;
352752
- return Boolean(v);
352753
- }
352754
- function cleanTag(inner) {
352755
- return inner.replace(/^-|\s-$/g, "").trim();
352756
- }
352757
- function tokenize3(tpl) {
352758
- const tokenRe = /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\})/g;
352759
- let lastIndex = 0;
352760
- const tokens = [];
352761
- let m2;
352762
- while ((m2 = tokenRe.exec(tpl)) !== null) {
352763
- if (m2.index > lastIndex) tokens.push({ type: "text", value: tpl.slice(lastIndex, m2.index) });
352764
- const tok = m2[0];
352765
- if (tok.startsWith("{%")) tokens.push({ type: "tag", value: cleanTag(tok.slice(2, -2)) });
352766
- else tokens.push({ type: "expr", value: tok.slice(2, -2).trim() });
352767
- lastIndex = m2.index + tok.length;
352768
- }
352769
- if (lastIndex < tpl.length) tokens.push({ type: "text", value: tpl.slice(lastIndex) });
352770
- return tokens;
352771
- }
352772
- function findMatchingEnd(toks, start, open, close) {
352773
- let depth = 1;
352774
- for (let j = start + 1; j < toks.length; j++) {
352775
- if (toks[j].type !== "tag") continue;
352776
- const f = toks[j].value.split(/\s+/)[0];
352777
- if (f === open) depth++;
352778
- else if (f === close) {
352779
- depth--;
352780
- if (depth === 0) return j;
352781
- }
352782
- }
352783
- throw new Error(`Unbalanced {% ${open} %}: missing {% ${close} %}`);
352784
- }
352785
- function findMatchingEndIf(toks, start) {
352786
- let depth = 1;
352787
- for (let j = start + 1; j < toks.length; j++) {
352788
- if (toks[j].type !== "tag") continue;
352789
- const f = toks[j].value.split(/\s+/)[0];
352790
- if (f === "if") depth++;
352791
- else if (f === "endif") {
352792
- depth--;
352793
- if (depth === 0) return j;
352794
- }
352795
- }
352796
- throw new Error("Unbalanced {% if %}: missing {% endif %}");
352797
- }
352798
- function splitIfElse(body) {
352799
- const ifBody = [];
352800
- const elseBody = [];
352801
- const elifs = [];
352802
- let cur = ifBody;
352803
- let curElif = null;
352804
- let depth = 0;
352805
- for (const t of body) {
352806
- if (t.type === "tag") {
352807
- const f = t.value.split(/\s+/)[0];
352808
- if (f === "if") depth++;
352809
- if (f === "endif") depth = Math.max(0, depth - 1);
352810
- if (depth === 0 && f === "elif") {
352811
- const cond = t.value.replace(/^elif\s+/, "");
352812
- curElif = { cond, body: [] };
352813
- elifs.push(curElif);
352814
- cur = curElif.body;
352815
- continue;
352816
- }
352817
- if (depth === 0 && f === "else") {
352818
- cur = elseBody;
352819
- continue;
352820
- }
352821
- }
352822
- cur.push(t);
352823
- }
352824
- return { ifBody, elseBody, elifs };
352825
- }
352826
- function renderTemplate2(tpl, vars = {}) {
352827
- const tokens = tokenize3(tpl);
352828
- function renderTokens2(toks, scope) {
352829
- let s = "";
352830
- let idx = 0;
352831
- while (idx < toks.length) {
352832
- const t = toks[idx];
352833
- if (t.type === "text") {
352834
- s += t.value;
352835
- idx++;
352836
- continue;
352837
- }
352838
- if (t.type === "expr") {
352839
- s += evalExpr(t.value, scope);
352840
- idx++;
352841
- continue;
352842
- }
352843
- const tag = t.value;
352844
- const first = tag.split(/\s+/)[0];
352845
- if (first === "for") {
352846
- const fm = tag.match(/^for\s+(\w+)\s+in\s+(.+)$/);
352847
- const fk = tag.match(/^for\s+(\w+)\s*,\s*(\w+)\s+in\s+(.+?)\.items\(\)$/);
352848
- const bodyEnd = findMatchingEnd(toks, idx, "for", "endfor");
352849
- const body = toks.slice(idx + 1, bodyEnd);
352850
- idx = bodyEnd + 1;
352851
- if (fk) {
352852
- const kName = fk[1];
352853
- const vName = fk[2];
352854
- const obj = resolve(fk[3], scope);
352855
- if (obj && typeof obj === "object") {
352856
- for (const [k, v] of Object.entries(obj)) {
352857
- s += renderTokens2(body, { ...scope, [kName]: k, [vName]: v });
352858
- }
352859
- }
352860
- } else if (fm) {
352861
- const name = fm[1];
352862
- const list = resolve(fm[2], scope);
352863
- if (Array.isArray(list)) {
352864
- for (const item of list) s += renderTokens2(body, { ...scope, [name]: item });
352865
- } else if (typeof list === "string") {
352866
- for (const ch of list) s += renderTokens2(body, { ...scope, [name]: ch });
352867
- } else if (list && typeof list === "object") {
352868
- for (const [k, v] of Object.entries(list)) {
352869
- s += renderTokens2(body, { ...scope, [name]: { key: k, value: v } });
352870
- }
352871
- }
352872
- }
352873
- continue;
352874
- }
352875
- if (first === "if") {
352876
- const cond = tag.replace(/^if\s+/, "");
352877
- const bodyEnd = findMatchingEndIf(toks, idx);
352878
- const { ifBody, elseBody, elifs } = splitIfElse(toks.slice(idx + 1, bodyEnd));
352879
- idx = bodyEnd + 1;
352880
- let rendered = false;
352881
- if (evalCond(cond, scope)) {
352882
- s += renderTokens2(ifBody, scope);
352883
- rendered = true;
352884
- } else {
352885
- for (const e of elifs) {
352886
- if (evalCond(e.cond, scope)) {
352887
- s += renderTokens2(e.body, scope);
352888
- rendered = true;
352889
- break;
352890
- }
352891
- }
352892
- if (!rendered && elseBody.length) s += renderTokens2(elseBody, scope);
352893
- }
352894
- continue;
352895
- }
352896
- idx++;
352897
- }
352898
- return s;
352899
- }
352900
- return renderTokens2(tokens, vars);
352901
- }
352902
- function diffStrings(a, b) {
352903
- const aLines = a.split("\n");
352904
- const bLines = b.split("\n");
352905
- const lines = [];
352906
- const max = Math.max(aLines.length, bLines.length);
352907
- for (let i = 0; i < max; i++) {
352908
- const al = aLines[i];
352909
- const bl = bLines[i];
352910
- if (al === bl) {
352911
- if (al !== void 0) lines.push(` ${al}`);
352912
- } else {
352913
- if (al !== void 0) lines.push(`- ${al}`);
352914
- if (bl !== void 0) lines.push(`+ ${bl}`);
352915
- }
352916
- }
352917
- return lines.join("\n");
352918
- }
353186
+ init_templateEngine();
352919
353187
 
352920
353188
  // ../../packages/backend/src/services/automation/puttyImport.ts
352921
353189
  function decodeSessionName(name) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "neuralos",
3
- "version": "3.2.16",
4
- "description": "Headless AI-native backend for RTerm / neuralOS — v3.2.16: scheduled-task (cron) overhaul — timezone-aware evaluation, overlap guard, pause windows, catch-up opt-in, cron validation, run-now, run history + success rates, failure-streak alerting, task→playbook binding, onSuccess/onFailure chaining, drift detection, retry wiring. Model-settings Base URL prefill. 14 plugins, 61 plugin tools.",
3
+ "version": "3.2.18",
4
+ "description": "Headless AI-native backend for RTerm / neuralOS — v3.2.18: cross-session history search, model failover, sub-agent delegation, REST API layer, gateway rate limiting, settings backup/restore, idle terminal timeout. 14 plugins, 61 plugin tools, parallel tool execution, captureStatus.",
5
5
  "main": "bin/gybackend.cjs",
6
6
  "bin": { "gybackend": "bin/gybackend.cjs" },
7
7
  "license": "MIT",