replicas-cli 0.2.327 → 0.2.329
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.
- package/dist/index.mjs +738 -146
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -8086,7 +8086,7 @@ Anything else gets \`exec\`'d verbatim, so \`replicas computer launch xeyes\` wo
|
|
|
8086
8086
|
When opening a known page, prefer passing the URL directly: \`replicas computer launch chrome http://localhost:3000/\`.
|
|
8087
8087
|
|
|
8088
8088
|
### \`replicas computer record start <path> [--fps N]\`
|
|
8089
|
-
Starts an ffmpeg screen recorder. Output is a fragmented MP4 (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
8089
|
+
Starts an ffmpeg screen recorder. Output is post-processed when stopped: action windows stay at normal speed, idle gaps accelerate, click moments get eased camera zoom, and a synthetic cursor animates between logged mouse positions. The raw capture is fragmented MP4 while active (still playable if the workspace dies mid-record). Default 60fps; drop to 30 if the workspace is CPU-constrained.
|
|
8090
8090
|
|
|
8091
8091
|
Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
|
|
8092
8092
|
|
|
@@ -9307,7 +9307,7 @@ replicas automation create "Review my MRs" \\
|
|
|
9307
9307
|
replicas automation create ... --disabled
|
|
9308
9308
|
|
|
9309
9309
|
# Workspace lifecycle
|
|
9310
|
-
replicas automation create ... --lifecycle
|
|
9310
|
+
replicas automation create ... --lifecycle archive_when_done
|
|
9311
9311
|
replicas automation create ... --lifecycle sleep_when_done
|
|
9312
9312
|
replicas automation create ... --lifecycle delete_after_inactivity --auto-stop-minutes 30
|
|
9313
9313
|
\`\`\`
|
|
@@ -9537,7 +9537,7 @@ var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
|
9537
9537
|
var REPLICAS_CONFIG_FILENAMES = ["replicas.json", "replicas.yaml", "replicas.yml"];
|
|
9538
9538
|
|
|
9539
9539
|
// ../shared/src/cli-version.ts
|
|
9540
|
-
var CLI_VERSION = "0.2.
|
|
9540
|
+
var CLI_VERSION = "0.2.329";
|
|
9541
9541
|
|
|
9542
9542
|
// ../shared/src/engine/environment.ts
|
|
9543
9543
|
var DESKTOP_NOVNC_PORT = 6080;
|
|
@@ -9562,7 +9562,7 @@ var WORKSPACE_STATUSES = ["active", "sleeping", "archived", "preparing", "error"
|
|
|
9562
9562
|
function isWorkspaceSuspendedStatus(status) {
|
|
9563
9563
|
return status === "sleeping" || status === "archived";
|
|
9564
9564
|
}
|
|
9565
|
-
var VALID_LIFECYCLE_POLICIES = ["default", "
|
|
9565
|
+
var VALID_LIFECYCLE_POLICIES = ["default", "archive_when_done", "sleep_when_done", "delete_after_inactivity"];
|
|
9566
9566
|
function workspaceConfigWithCapabilities(config2, capabilities = {}) {
|
|
9567
9567
|
const mergedCapabilities = {
|
|
9568
9568
|
...config2?.capabilities,
|
|
@@ -13658,8 +13658,7 @@ function printAutomation(automation2) {
|
|
|
13658
13658
|
console.log(chalk18.gray(` Next Run: ${formatDate2(automation2.cron_next_fire_at)}`));
|
|
13659
13659
|
}
|
|
13660
13660
|
if (automation2.workspace_lifecycle_policy && automation2.workspace_lifecycle_policy !== "default") {
|
|
13661
|
-
|
|
13662
|
-
console.log(chalk18.gray(` Lifecycle: ${lifecycle}`));
|
|
13661
|
+
console.log(chalk18.gray(` Lifecycle: ${automation2.workspace_lifecycle_policy}`));
|
|
13663
13662
|
}
|
|
13664
13663
|
if (automation2.agent_provider || automation2.model || automation2.thinking_level) {
|
|
13665
13664
|
console.log(chalk18.gray(` Agent: ${formatAgentSummary(automation2)}`));
|
|
@@ -14482,12 +14481,17 @@ async function mediaListCommand(options) {
|
|
|
14482
14481
|
}
|
|
14483
14482
|
}
|
|
14484
14483
|
|
|
14485
|
-
// src/commands/computer.ts
|
|
14486
|
-
import { spawn as
|
|
14484
|
+
// src/commands/computer/index.ts
|
|
14485
|
+
import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
|
|
14487
14486
|
import { createHash as createHash2 } from "crypto";
|
|
14488
|
-
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "fs";
|
|
14489
|
-
import { dirname
|
|
14487
|
+
import { closeSync, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
14488
|
+
import { dirname as dirname2 } from "path";
|
|
14490
14489
|
import chalk20 from "chalk";
|
|
14490
|
+
|
|
14491
|
+
// src/commands/computer/desktop.ts
|
|
14492
|
+
import { spawnSync } from "child_process";
|
|
14493
|
+
import { existsSync } from "fs";
|
|
14494
|
+
import { isAbsolute, resolve } from "path";
|
|
14491
14495
|
var STATE_DIR = process.env.REPLICAS_DESKTOP_STATE_DIR || "/tmp/replicas-computer";
|
|
14492
14496
|
var DEFAULT_DISPLAY = process.env.REPLICAS_DESKTOP_DISPLAY || ":99";
|
|
14493
14497
|
var NOVNC_PORT = process.env.REPLICAS_DESKTOP_NOVNC_PORT ? parseInt(process.env.REPLICAS_DESKTOP_NOVNC_PORT, 10) : DESKTOP_NOVNC_PORT;
|
|
@@ -14548,6 +14552,23 @@ function getDisplayDimensions() {
|
|
|
14548
14552
|
if (!match) fail("could not read display dimensions from xdpyinfo");
|
|
14549
14553
|
return { width: Number.parseInt(match[1], 10), height: Number.parseInt(match[2], 10) };
|
|
14550
14554
|
}
|
|
14555
|
+
function getMouseLocation() {
|
|
14556
|
+
const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
|
|
14557
|
+
if (!out) return null;
|
|
14558
|
+
const values = Object.fromEntries(
|
|
14559
|
+
out.split("\n").map((line) => {
|
|
14560
|
+
const [key, value] = line.split("=");
|
|
14561
|
+
return [key.toLowerCase(), Number.parseInt(value, 10)];
|
|
14562
|
+
})
|
|
14563
|
+
);
|
|
14564
|
+
if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
|
|
14565
|
+
return {
|
|
14566
|
+
x: values.x,
|
|
14567
|
+
y: values.y,
|
|
14568
|
+
screen: Number.isFinite(values.screen) ? values.screen : 0,
|
|
14569
|
+
window: Number.isFinite(values.window) ? values.window : 0
|
|
14570
|
+
};
|
|
14571
|
+
}
|
|
14551
14572
|
function parseScreenCoord(value, label, size) {
|
|
14552
14573
|
if (value.endsWith("%")) {
|
|
14553
14574
|
const rawPct = value.slice(0, -1);
|
|
@@ -14563,8 +14584,655 @@ function parseScreenCoord(value, label, size) {
|
|
|
14563
14584
|
function resolvePath(p) {
|
|
14564
14585
|
return isAbsolute(p) ? p : resolve(process.cwd(), p);
|
|
14565
14586
|
}
|
|
14587
|
+
function configuredDesktopDimensions() {
|
|
14588
|
+
const width = parseInt(process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH), 10);
|
|
14589
|
+
const height = parseInt(process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT), 10);
|
|
14590
|
+
if (!Number.isFinite(width) || width <= 0) fail(`REPLICAS_DESKTOP_WIDTH must be a positive integer`);
|
|
14591
|
+
if (!Number.isFinite(height) || height <= 0) fail(`REPLICAS_DESKTOP_HEIGHT must be a positive integer`);
|
|
14592
|
+
return { width, height };
|
|
14593
|
+
}
|
|
14594
|
+
function clamp(n, min, max) {
|
|
14595
|
+
return Math.min(max, Math.max(min, n));
|
|
14596
|
+
}
|
|
14597
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14598
|
+
|
|
14599
|
+
// src/commands/computer/recording.ts
|
|
14600
|
+
import { spawn as spawn3 } from "child_process";
|
|
14601
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14602
|
+
import { dirname } from "path";
|
|
14603
|
+
|
|
14604
|
+
// src/commands/computer/recording/render.ts
|
|
14605
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
14606
|
+
import { copyFileSync, rmSync, writeFileSync } from "fs";
|
|
14607
|
+
|
|
14608
|
+
// src/commands/computer/recording/config.ts
|
|
14609
|
+
var cameraMotion = {
|
|
14610
|
+
clickLeadSeconds: 1.2,
|
|
14611
|
+
clickTailSeconds: 0.5,
|
|
14612
|
+
actionLeadSeconds: 0.9,
|
|
14613
|
+
clickZoom: 1.24,
|
|
14614
|
+
closeClickOutputGapSeconds: 6,
|
|
14615
|
+
clickMoveSeconds: 1.2,
|
|
14616
|
+
returnSeconds: 1,
|
|
14617
|
+
minSegmentSeconds: 0.05
|
|
14618
|
+
};
|
|
14619
|
+
var cursorMotion = {
|
|
14620
|
+
minMoveSeconds: 0.38,
|
|
14621
|
+
maxMoveSeconds: 1.7,
|
|
14622
|
+
speedPxPerSecond: 700,
|
|
14623
|
+
defaultLeadSeconds: 0.16,
|
|
14624
|
+
clickShrinkSeconds: 0.08,
|
|
14625
|
+
clickGrowSeconds: 0.14,
|
|
14626
|
+
clickDwellGapStartSeconds: 0.75,
|
|
14627
|
+
clickDwellFullGapSeconds: 3.2,
|
|
14628
|
+
clickMinDwellSeconds: 0.12,
|
|
14629
|
+
clickMaxDwellSeconds: 1.15,
|
|
14630
|
+
maxDwellGapShare: 0.45,
|
|
14631
|
+
dragLeadSeconds: 0.55,
|
|
14632
|
+
maxKeyframes: 120
|
|
14633
|
+
};
|
|
14634
|
+
var cursorStyle = {
|
|
14635
|
+
size: 30,
|
|
14636
|
+
clickSize: 24
|
|
14637
|
+
};
|
|
14638
|
+
|
|
14639
|
+
// src/commands/computer/recording/ffmpeg-expressions.ts
|
|
14640
|
+
function expressionNumber(n) {
|
|
14641
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(3);
|
|
14642
|
+
}
|
|
14643
|
+
function mixNumber(from, to, progress) {
|
|
14644
|
+
return from + (to - from) * progress;
|
|
14645
|
+
}
|
|
14646
|
+
function easeInOutSmootherNumber(progress) {
|
|
14647
|
+
const p = clamp(progress, 0, 1);
|
|
14648
|
+
return p * p * p * (p * (p * 6 - 15) + 10);
|
|
14649
|
+
}
|
|
14650
|
+
function easeInOutSmootherExpression(progress) {
|
|
14651
|
+
const p = `(${progress})`;
|
|
14652
|
+
return `${p}*${p}*${p}*(${p}*(${p}*6-15)+10)`;
|
|
14653
|
+
}
|
|
14654
|
+
function progressOverTime(timeExpression, seconds) {
|
|
14655
|
+
return easeInOutSmootherExpression(`min(max((${timeExpression})/${seconds.toFixed(3)}\\,0)\\,1)`);
|
|
14656
|
+
}
|
|
14657
|
+
function progressBetween(start, duration) {
|
|
14658
|
+
return easeInOutSmootherExpression(`min(max((t-${start.toFixed(3)})/${duration.toFixed(3)}\\,0)\\,1)`);
|
|
14659
|
+
}
|
|
14660
|
+
function mixExpression(from, to, progress) {
|
|
14661
|
+
return `${expressionNumber(from)}+(${expressionNumber(to - from)})*${progress}`;
|
|
14662
|
+
}
|
|
14663
|
+
function cropOrigin(point, zoom, size) {
|
|
14664
|
+
return clamp(point * zoom - size / 2, 0, size * zoom - size);
|
|
14665
|
+
}
|
|
14666
|
+
function easedCropOrigin(fromPoint, toPoint, fromZoom, toZoom, size, progress) {
|
|
14667
|
+
return mixExpression(
|
|
14668
|
+
cropOrigin(fromPoint, fromZoom, size),
|
|
14669
|
+
cropOrigin(toPoint, toZoom, size),
|
|
14670
|
+
progress
|
|
14671
|
+
);
|
|
14672
|
+
}
|
|
14673
|
+
function betweenExpression(start, end) {
|
|
14674
|
+
return `between(t\\,${start.toFixed(3)}\\,${end.toFixed(3)})`;
|
|
14675
|
+
}
|
|
14676
|
+
|
|
14677
|
+
// src/commands/computer/recording/timeline.ts
|
|
14678
|
+
function centerOf(size) {
|
|
14679
|
+
return { x: size.width / 2, y: size.height / 2 };
|
|
14680
|
+
}
|
|
14681
|
+
function actionPoint(action, size) {
|
|
14682
|
+
const x = action.toX ?? action.x ?? size.width / 2;
|
|
14683
|
+
const y = action.toY ?? action.y ?? size.height / 2;
|
|
14684
|
+
return { x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) };
|
|
14685
|
+
}
|
|
14686
|
+
function idleSpeed(duration) {
|
|
14687
|
+
if (duration < 1.2) return 1;
|
|
14688
|
+
return clamp(1 + Math.log1p(duration - 0.8) * 1.8, 1.5, 5);
|
|
14689
|
+
}
|
|
14690
|
+
function renderedGapSeconds(duration) {
|
|
14691
|
+
return duration / idleSpeed(duration);
|
|
14692
|
+
}
|
|
14693
|
+
function timelineActions(actions) {
|
|
14694
|
+
return actions.filter((action) => action.type !== "key" && action.type !== "move");
|
|
14695
|
+
}
|
|
14696
|
+
function actionWindow(action, duration) {
|
|
14697
|
+
const shouldZoom = action.type === "click";
|
|
14698
|
+
const at = clamp(action.atMs / 1e3, 0, duration);
|
|
14699
|
+
const start = clamp(at - (shouldZoom ? cameraMotion.clickLeadSeconds : cameraMotion.actionLeadSeconds), 0, duration);
|
|
14700
|
+
const tail = shouldZoom ? cameraMotion.clickTailSeconds : action.type === "type" ? 1.5 : 0.9;
|
|
14701
|
+
return { start, end: clamp(at + tail, start, duration), shouldZoom };
|
|
14702
|
+
}
|
|
14703
|
+
function nextClickStart(actions, index, duration) {
|
|
14704
|
+
const nextClick = actions.slice(index + 1).find((candidate) => candidate.type === "click");
|
|
14705
|
+
return nextClick ? clamp(nextClick.atMs / 1e3 - cameraMotion.clickLeadSeconds, 0, duration) : null;
|
|
14706
|
+
}
|
|
14707
|
+
function shouldPreserveZoom(nextStart, cursor, lastZoom) {
|
|
14708
|
+
return lastZoom > 1 && nextStart !== null && nextStart > cursor && renderedGapSeconds(nextStart - cursor) <= cameraMotion.closeClickOutputGapSeconds;
|
|
14709
|
+
}
|
|
14710
|
+
function fullFrameSegment(duration, size) {
|
|
14711
|
+
const center = centerOf(size);
|
|
14712
|
+
return {
|
|
14713
|
+
start: 0,
|
|
14714
|
+
end: duration,
|
|
14715
|
+
speed: 1,
|
|
14716
|
+
motionSeconds: cameraMotion.returnSeconds,
|
|
14717
|
+
fromZoom: 1,
|
|
14718
|
+
zoom: 1,
|
|
14719
|
+
fromX: center.x,
|
|
14720
|
+
fromY: center.y,
|
|
14721
|
+
...center
|
|
14722
|
+
};
|
|
14723
|
+
}
|
|
14724
|
+
function buildRecordingSegments(actions, duration, size) {
|
|
14725
|
+
const actionsForTimeline = timelineActions(actions);
|
|
14726
|
+
if (actionsForTimeline.length === 0) return [fullFrameSegment(duration, size)];
|
|
14727
|
+
const center = centerOf(size);
|
|
14728
|
+
const segments = [];
|
|
14729
|
+
let cursor = 0;
|
|
14730
|
+
let lastPoint = center;
|
|
14731
|
+
let lastZoom = 1;
|
|
14732
|
+
for (let i = 0; i < actionsForTimeline.length; i++) {
|
|
14733
|
+
const action = actionsForTimeline[i];
|
|
14734
|
+
const { start, end, shouldZoom } = actionWindow(action, duration);
|
|
14735
|
+
const preserveZoom = !shouldZoom && shouldPreserveZoom(nextClickStart(actionsForTimeline, i, duration), cursor, lastZoom);
|
|
14736
|
+
const point = shouldZoom ? actionPoint(action, size) : preserveZoom ? lastPoint : center;
|
|
14737
|
+
const zoom = shouldZoom ? cameraMotion.clickZoom : preserveZoom ? lastZoom : 1;
|
|
14738
|
+
if (start > cursor + cameraMotion.minSegmentSeconds) {
|
|
14739
|
+
const gap = start - cursor;
|
|
14740
|
+
const speed = idleSpeed(gap);
|
|
14741
|
+
const bridgeZoom = shouldZoom && lastZoom > 1 && renderedGapSeconds(gap) <= cameraMotion.closeClickOutputGapSeconds;
|
|
14742
|
+
segments.push({
|
|
14743
|
+
start: cursor,
|
|
14744
|
+
end: start,
|
|
14745
|
+
speed,
|
|
14746
|
+
motionSeconds: bridgeZoom ? Math.min(cameraMotion.clickMoveSeconds, Math.max(0.18, gap / speed)) : cameraMotion.returnSeconds,
|
|
14747
|
+
fromZoom: lastZoom,
|
|
14748
|
+
zoom: bridgeZoom ? cameraMotion.clickZoom : zoom,
|
|
14749
|
+
fromX: lastPoint.x,
|
|
14750
|
+
fromY: lastPoint.y,
|
|
14751
|
+
...point
|
|
14752
|
+
});
|
|
14753
|
+
lastZoom = bridgeZoom ? cameraMotion.clickZoom : zoom;
|
|
14754
|
+
lastPoint = point;
|
|
14755
|
+
}
|
|
14756
|
+
const actionStart = Math.max(cursor, start);
|
|
14757
|
+
if (end > actionStart + cameraMotion.minSegmentSeconds) {
|
|
14758
|
+
segments.push({
|
|
14759
|
+
start: actionStart,
|
|
14760
|
+
end,
|
|
14761
|
+
speed: 1,
|
|
14762
|
+
motionSeconds: shouldZoom ? cameraMotion.clickMoveSeconds : cameraMotion.returnSeconds,
|
|
14763
|
+
fromZoom: lastZoom,
|
|
14764
|
+
zoom,
|
|
14765
|
+
fromX: lastPoint.x,
|
|
14766
|
+
fromY: lastPoint.y,
|
|
14767
|
+
...point
|
|
14768
|
+
});
|
|
14769
|
+
cursor = end;
|
|
14770
|
+
lastZoom = zoom;
|
|
14771
|
+
}
|
|
14772
|
+
lastPoint = point;
|
|
14773
|
+
}
|
|
14774
|
+
if (duration > cursor + cameraMotion.minSegmentSeconds) {
|
|
14775
|
+
segments.push({
|
|
14776
|
+
start: cursor,
|
|
14777
|
+
end: duration,
|
|
14778
|
+
speed: idleSpeed(duration - cursor),
|
|
14779
|
+
motionSeconds: cameraMotion.returnSeconds,
|
|
14780
|
+
fromZoom: lastZoom,
|
|
14781
|
+
zoom: 1,
|
|
14782
|
+
fromX: lastPoint.x,
|
|
14783
|
+
fromY: lastPoint.y,
|
|
14784
|
+
...center
|
|
14785
|
+
});
|
|
14786
|
+
}
|
|
14787
|
+
return segments.filter((segment) => segment.end > segment.start + cameraMotion.minSegmentSeconds);
|
|
14788
|
+
}
|
|
14789
|
+
|
|
14790
|
+
// src/commands/computer/recording/cursor.ts
|
|
14791
|
+
var cursorHotspotRatio = { x: 8 / 32, y: 4 / 32 };
|
|
14792
|
+
function cursorAssets(stamp) {
|
|
14793
|
+
return {
|
|
14794
|
+
path: `/tmp/replicas-cursor-${stamp}.svg`,
|
|
14795
|
+
size: cursorStyle.size
|
|
14796
|
+
};
|
|
14797
|
+
}
|
|
14798
|
+
function clickDwellSeconds(gapSeconds) {
|
|
14799
|
+
if (gapSeconds <= cursorMotion.clickDwellGapStartSeconds) return cursorMotion.defaultLeadSeconds;
|
|
14800
|
+
const progress = (gapSeconds - cursorMotion.clickDwellGapStartSeconds) / (cursorMotion.clickDwellFullGapSeconds - cursorMotion.clickDwellGapStartSeconds);
|
|
14801
|
+
const dwell = mixNumber(
|
|
14802
|
+
cursorMotion.clickMinDwellSeconds,
|
|
14803
|
+
cursorMotion.clickMaxDwellSeconds,
|
|
14804
|
+
easeInOutSmootherNumber(progress)
|
|
14805
|
+
);
|
|
14806
|
+
return Math.min(dwell, gapSeconds * cursorMotion.maxDwellGapShare);
|
|
14807
|
+
}
|
|
14808
|
+
function arrivalLeadSeconds(action, gapSeconds) {
|
|
14809
|
+
return action.type === "click" ? clickDwellSeconds(gapSeconds) : cursorMotion.defaultLeadSeconds;
|
|
14810
|
+
}
|
|
14811
|
+
function cursorKeyframe(action, at, previousAt, size) {
|
|
14812
|
+
if (typeof action.x !== "number" || typeof action.y !== "number") return null;
|
|
14813
|
+
const point = actionPoint(action, size);
|
|
14814
|
+
const lead = arrivalLeadSeconds(action, at - previousAt);
|
|
14815
|
+
return { ...point, at, arriveAt: clamp(at - lead, previousAt, at) };
|
|
14816
|
+
}
|
|
14817
|
+
function dedupeKeyframes(points) {
|
|
14818
|
+
return points.sort((a, b) => a.at - b.at).filter(
|
|
14819
|
+
(point, index, all) => index === 0 || point.at - all[index - 1].at > 0.08 || Math.hypot(point.x - all[index - 1].x, point.y - all[index - 1].y) > 24
|
|
14820
|
+
);
|
|
14821
|
+
}
|
|
14822
|
+
function downsampleKeyframes(points, actions, size) {
|
|
14823
|
+
if (points.length <= cursorMotion.maxKeyframes) return points;
|
|
14824
|
+
const anchors = actions.filter((action) => action.type !== "move").map((action) => actionPoint(action, size));
|
|
14825
|
+
const result = points.filter(
|
|
14826
|
+
(point, index) => index === 0 || index === points.length - 1 || anchors.some((anchor) => Math.hypot(anchor.x - point.x, anchor.y - point.y) < 1)
|
|
14827
|
+
);
|
|
14828
|
+
const step = points.length / Math.max(1, cursorMotion.maxKeyframes - result.length);
|
|
14829
|
+
for (let i = 0; result.length < cursorMotion.maxKeyframes && i < points.length; i += step) {
|
|
14830
|
+
const point = points[Math.floor(i)];
|
|
14831
|
+
if (!result.includes(point)) result.push(point);
|
|
14832
|
+
}
|
|
14833
|
+
return result.sort((a, b) => a.at - b.at);
|
|
14834
|
+
}
|
|
14835
|
+
function cursorKeyframes(actions, duration, size) {
|
|
14836
|
+
const center = { x: size.width / 2, y: size.height / 2 };
|
|
14837
|
+
const points = [{ at: 0, arriveAt: 0, ...center }];
|
|
14838
|
+
for (const action of actions) {
|
|
14839
|
+
const at = clamp(action.atMs / 1e3, 0, duration);
|
|
14840
|
+
const previousAt = points[points.length - 1].at;
|
|
14841
|
+
if (action.type === "drag" && typeof action.x === "number" && typeof action.y === "number") {
|
|
14842
|
+
const startAt = clamp(at - cursorMotion.dragLeadSeconds, 0, duration);
|
|
14843
|
+
points.push({
|
|
14844
|
+
at: startAt,
|
|
14845
|
+
arriveAt: startAt,
|
|
14846
|
+
x: clamp(action.x, 0, size.width),
|
|
14847
|
+
y: clamp(action.y, 0, size.height)
|
|
14848
|
+
});
|
|
14849
|
+
if (typeof action.toX === "number" && typeof action.toY === "number") {
|
|
14850
|
+
points.push({
|
|
14851
|
+
at,
|
|
14852
|
+
arriveAt: at,
|
|
14853
|
+
x: clamp(action.toX, 0, size.width),
|
|
14854
|
+
y: clamp(action.toY, 0, size.height)
|
|
14855
|
+
});
|
|
14856
|
+
}
|
|
14857
|
+
continue;
|
|
14858
|
+
}
|
|
14859
|
+
const point = cursorKeyframe(action, at, previousAt, size);
|
|
14860
|
+
if (point) points.push(point);
|
|
14861
|
+
}
|
|
14862
|
+
return downsampleKeyframes(dedupeKeyframes(points), actions, size);
|
|
14863
|
+
}
|
|
14864
|
+
function cursorAxisExpression(points, axis) {
|
|
14865
|
+
if (points.length === 0) return "0";
|
|
14866
|
+
let expression = expressionNumber(points[points.length - 1][axis]);
|
|
14867
|
+
for (let i = points.length - 2; i >= 0; i--) {
|
|
14868
|
+
const from = points[i];
|
|
14869
|
+
const to = points[i + 1];
|
|
14870
|
+
const arriveAt = clamp(to.arriveAt, from.at, to.at);
|
|
14871
|
+
const moveWindow = Math.max(1e-3, arriveAt - from.at);
|
|
14872
|
+
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
14873
|
+
const moveSeconds = Math.min(
|
|
14874
|
+
moveWindow,
|
|
14875
|
+
Math.max(cursorMotion.minMoveSeconds, Math.min(cursorMotion.maxMoveSeconds, distance / cursorMotion.speedPxPerSecond))
|
|
14876
|
+
);
|
|
14877
|
+
const moveStart = Math.max(from.at, arriveAt - moveSeconds);
|
|
14878
|
+
const duration = Math.max(1e-3, arriveAt - moveStart);
|
|
14879
|
+
const interpolated = mixExpression(from[axis], to[axis], progressBetween(moveStart, duration));
|
|
14880
|
+
expression = `if(lte(t\\,${moveStart.toFixed(3)})\\,${expressionNumber(from[axis])}\\,if(lte(t\\,${to.at.toFixed(3)})\\,${interpolated}\\,${expression}))`;
|
|
14881
|
+
}
|
|
14882
|
+
return expression;
|
|
14883
|
+
}
|
|
14884
|
+
function cursorSizeExpression(actions, normalSize) {
|
|
14885
|
+
let expression = expressionNumber(normalSize);
|
|
14886
|
+
const clicks = actions.filter((action) => action.type === "click").sort((a, b) => b.atMs - a.atMs);
|
|
14887
|
+
for (const action of clicks) {
|
|
14888
|
+
const at = action.atMs / 1e3;
|
|
14889
|
+
const shrinkStart = Math.max(0, at - cursorMotion.clickShrinkSeconds);
|
|
14890
|
+
const shrink = mixExpression(
|
|
14891
|
+
normalSize,
|
|
14892
|
+
cursorStyle.clickSize,
|
|
14893
|
+
progressBetween(shrinkStart, Math.max(1e-3, at - shrinkStart))
|
|
14894
|
+
);
|
|
14895
|
+
const grow = mixExpression(cursorStyle.clickSize, normalSize, progressBetween(at, cursorMotion.clickGrowSeconds));
|
|
14896
|
+
expression = `if(${betweenExpression(shrinkStart, at)}\\,${shrink}\\,if(${betweenExpression(at, at + cursorMotion.clickGrowSeconds)}\\,${grow}\\,${expression}))`;
|
|
14897
|
+
}
|
|
14898
|
+
return expression;
|
|
14899
|
+
}
|
|
14900
|
+
function cursorOverlayPositionExpression(cursor, size, hotspotRatio, overlaySize) {
|
|
14901
|
+
return `max(0\\,min(${expressionNumber(size)}-${overlaySize}\\,(${cursor})-${hotspotRatio.toFixed(6)}*${overlaySize}))`;
|
|
14902
|
+
}
|
|
14903
|
+
function cursorSvg(size) {
|
|
14904
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7.7 3.4c-1.45 0-2.62 1.17-2.62 2.62v19.7c0 2.46 3.02 3.65 4.7 1.85l5.06-5.44a5.42 5.42 0 0 1 3.96-1.73h5.82c2.48 0 3.63-3.08 1.76-4.7L9.42 4.04A2.62 2.62 0 0 0 7.7 3.4Z" fill="white" fill-opacity="0.98" stroke="black" stroke-opacity="0.88" stroke-width="2.9" stroke-linejoin="round"/></svg>`;
|
|
14905
|
+
}
|
|
14906
|
+
function buildCursorOverlayFilter(actions, duration, size, assets, projectCursor = null, inputLabel = "[screen]", outputLabel = "[v]") {
|
|
14907
|
+
const points = cursorKeyframes(actions, duration, size);
|
|
14908
|
+
const rawCursorX = cursorAxisExpression(points, "x");
|
|
14909
|
+
const rawCursorY = cursorAxisExpression(points, "y");
|
|
14910
|
+
const cursorX = projectCursor ? projectCursor.x(rawCursorX) : rawCursorX;
|
|
14911
|
+
const cursorY = projectCursor ? projectCursor.y(rawCursorY) : rawCursorY;
|
|
14912
|
+
const cursorSize = cursorSizeExpression(actions, assets.size);
|
|
14913
|
+
const cursorOverlayX = cursorOverlayPositionExpression(cursorX, size.width, cursorHotspotRatio.x, "overlay_w");
|
|
14914
|
+
const cursorOverlayY = cursorOverlayPositionExpression(cursorY, size.height, cursorHotspotRatio.y, "overlay_h");
|
|
14915
|
+
return `[1:v]scale=w='${cursorSize}':h='${cursorSize}':eval=frame[cursor];${inputLabel}[cursor]overlay=x='${cursorOverlayX}':y='${cursorOverlayY}':eof_action=repeat:shortest=1:eval=frame${outputLabel}`;
|
|
14916
|
+
}
|
|
14917
|
+
|
|
14918
|
+
// src/commands/computer/recording/render.ts
|
|
14919
|
+
function videoDuration(path6) {
|
|
14920
|
+
const r = spawnSync2("ffprobe", [
|
|
14921
|
+
"-v",
|
|
14922
|
+
"error",
|
|
14923
|
+
"-show_entries",
|
|
14924
|
+
"format=duration",
|
|
14925
|
+
"-of",
|
|
14926
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
14927
|
+
path6
|
|
14928
|
+
], { stdio: "pipe" });
|
|
14929
|
+
if (r.status !== 0) fail(`ffprobe failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
14930
|
+
const duration = Number.parseFloat(r.stdout?.toString().trim() ?? "");
|
|
14931
|
+
if (!Number.isFinite(duration) || duration <= 0) fail(`could not read recording duration for ${path6}`);
|
|
14932
|
+
return duration;
|
|
14933
|
+
}
|
|
14934
|
+
function renderedSegmentSpans(segments) {
|
|
14935
|
+
let outputStart = 0;
|
|
14936
|
+
return segments.map((segment) => {
|
|
14937
|
+
const outputEnd = outputStart + (segment.end - segment.start) / segment.speed;
|
|
14938
|
+
const span = {
|
|
14939
|
+
rawStart: segment.start,
|
|
14940
|
+
rawEnd: segment.end,
|
|
14941
|
+
outputStart,
|
|
14942
|
+
outputEnd,
|
|
14943
|
+
speed: segment.speed
|
|
14944
|
+
};
|
|
14945
|
+
outputStart = outputEnd;
|
|
14946
|
+
return span;
|
|
14947
|
+
});
|
|
14948
|
+
}
|
|
14949
|
+
function outputTimeForRawTime(spans, rawTime) {
|
|
14950
|
+
const span = spans.find((candidate) => rawTime >= candidate.rawStart && rawTime <= candidate.rawEnd);
|
|
14951
|
+
if (span) return span.outputStart + (rawTime - span.rawStart) / span.speed;
|
|
14952
|
+
if (spans.length === 0) return rawTime;
|
|
14953
|
+
if (rawTime <= spans[0].rawStart) return spans[0].outputStart;
|
|
14954
|
+
const lastSpan = spans[spans.length - 1];
|
|
14955
|
+
if (rawTime >= lastSpan.rawEnd) return lastSpan.outputEnd;
|
|
14956
|
+
return spans.find((candidate) => rawTime < candidate.rawStart)?.outputStart ?? lastSpan.outputEnd;
|
|
14957
|
+
}
|
|
14958
|
+
function actionsOnRenderedTimeline(actions, spans) {
|
|
14959
|
+
return actions.map((action) => ({
|
|
14960
|
+
...action,
|
|
14961
|
+
atMs: outputTimeForRawTime(spans, action.atMs / 1e3) * 1e3
|
|
14962
|
+
}));
|
|
14963
|
+
}
|
|
14964
|
+
function segmentMotionSeconds(segment) {
|
|
14965
|
+
return Math.min(segment.motionSeconds, Math.max(0.12, (segment.end - segment.start) / segment.speed));
|
|
14966
|
+
}
|
|
14967
|
+
function scaledFrameClampExpression(value, zoom, outputSize) {
|
|
14968
|
+
return `max(0\\,min(${outputSize}*(${zoom})-${outputSize}\\,${value}))`;
|
|
14969
|
+
}
|
|
14970
|
+
function cameraTransformExpressions(segment, size, timeExpression) {
|
|
14971
|
+
const progress = progressOverTime(timeExpression, segmentMotionSeconds(segment));
|
|
14972
|
+
const zoom = mixExpression(segment.fromZoom, segment.zoom, progress);
|
|
14973
|
+
return {
|
|
14974
|
+
zoom,
|
|
14975
|
+
cropX: scaledFrameClampExpression(
|
|
14976
|
+
easedCropOrigin(segment.fromX, segment.x, segment.fromZoom, segment.zoom, size.width, progress),
|
|
14977
|
+
zoom,
|
|
14978
|
+
size.width
|
|
14979
|
+
),
|
|
14980
|
+
cropY: scaledFrameClampExpression(
|
|
14981
|
+
easedCropOrigin(segment.fromY, segment.y, segment.fromZoom, segment.zoom, size.height, progress),
|
|
14982
|
+
zoom,
|
|
14983
|
+
size.height
|
|
14984
|
+
)
|
|
14985
|
+
};
|
|
14986
|
+
}
|
|
14987
|
+
function projectedCursorExpression(rawCursor, axis, segments, spans, size) {
|
|
14988
|
+
let expression = rawCursor;
|
|
14989
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
14990
|
+
const span = spans[i];
|
|
14991
|
+
const camera = cameraTransformExpressions(segments[i], size, `t-${span.outputStart.toFixed(3)}`);
|
|
14992
|
+
const crop = axis === "x" ? camera.cropX : camera.cropY;
|
|
14993
|
+
const projected = `(${rawCursor})*(${camera.zoom})-(${crop})`;
|
|
14994
|
+
expression = `if(${betweenExpression(span.outputStart, span.outputEnd)}\\,${projected}\\,${expression})`;
|
|
14995
|
+
}
|
|
14996
|
+
return expression;
|
|
14997
|
+
}
|
|
14998
|
+
function renderRecording(rawPath, target, actions, fps, size) {
|
|
14999
|
+
if (actions.length === 0) {
|
|
15000
|
+
copyFileSync(rawPath, target);
|
|
15001
|
+
return;
|
|
15002
|
+
}
|
|
15003
|
+
const duration = videoDuration(rawPath);
|
|
15004
|
+
const segments = buildRecordingSegments(actions, duration, size);
|
|
15005
|
+
if (segments.length === 0) {
|
|
15006
|
+
copyFileSync(rawPath, target);
|
|
15007
|
+
return;
|
|
15008
|
+
}
|
|
15009
|
+
const stamp = `${process.pid}-${Date.now()}`;
|
|
15010
|
+
const cursor = cursorAssets(stamp);
|
|
15011
|
+
writeFileSync(cursor.path, cursorSvg(cursor.size));
|
|
15012
|
+
const spans = renderedSegmentSpans(segments);
|
|
15013
|
+
const renderedDuration = spans.length ? spans[spans.length - 1].outputEnd : duration;
|
|
15014
|
+
const renderedActions = actionsOnRenderedTimeline(actions, spans);
|
|
15015
|
+
const cursorFilter = buildCursorOverlayFilter(
|
|
15016
|
+
renderedActions,
|
|
15017
|
+
renderedDuration,
|
|
15018
|
+
size,
|
|
15019
|
+
cursor,
|
|
15020
|
+
{
|
|
15021
|
+
x: (cursorX) => projectedCursorExpression(cursorX, "x", segments, spans, size),
|
|
15022
|
+
y: (cursorY) => projectedCursorExpression(cursorY, "y", segments, spans, size)
|
|
15023
|
+
}
|
|
15024
|
+
);
|
|
15025
|
+
const screenLabels = segments.map((_, index) => `[screen${index}]`).join("");
|
|
15026
|
+
const screenSplitFilter = `[0:v]split=${segments.length}${screenLabels}`;
|
|
15027
|
+
const filters = segments.map((segment, index) => {
|
|
15028
|
+
const { zoom, cropX, cropY } = cameraTransformExpressions(segment, size, "t");
|
|
15029
|
+
return `[screen${index}]trim=start=${segment.start.toFixed(3)}:end=${segment.end.toFixed(3)},setpts=(PTS-STARTPTS)/${segment.speed.toFixed(3)},scale=w=${size.width}*(${zoom}):h=${size.height}*(${zoom}):eval=frame,crop=${size.width}:${size.height}:x=${cropX}:y=${cropY},setsar=1[v${index}]`;
|
|
15030
|
+
});
|
|
15031
|
+
const concatInputs = segments.map((_, index) => `[v${index}]`).join("");
|
|
15032
|
+
const filter = `${screenSplitFilter};${filters.join(";")};${concatInputs}concat=n=${segments.length}:v=1:a=0[screen];${cursorFilter}`;
|
|
15033
|
+
const filterPath = `/tmp/replicas-recording-filter-${stamp}.ffgraph`;
|
|
15034
|
+
writeFileSync(filterPath, filter);
|
|
15035
|
+
try {
|
|
15036
|
+
const r = spawnSync2("ffmpeg", [
|
|
15037
|
+
"-y",
|
|
15038
|
+
"-hide_banner",
|
|
15039
|
+
"-loglevel",
|
|
15040
|
+
"warning",
|
|
15041
|
+
"-i",
|
|
15042
|
+
rawPath,
|
|
15043
|
+
"-loop",
|
|
15044
|
+
"1",
|
|
15045
|
+
"-i",
|
|
15046
|
+
cursor.path,
|
|
15047
|
+
"-filter_complex_script",
|
|
15048
|
+
filterPath,
|
|
15049
|
+
"-map",
|
|
15050
|
+
"[v]",
|
|
15051
|
+
"-r",
|
|
15052
|
+
String(fps),
|
|
15053
|
+
"-c:v",
|
|
15054
|
+
"libx264",
|
|
15055
|
+
"-preset",
|
|
15056
|
+
"veryfast",
|
|
15057
|
+
"-crf",
|
|
15058
|
+
"23",
|
|
15059
|
+
"-pix_fmt",
|
|
15060
|
+
"yuv420p",
|
|
15061
|
+
"-movflags",
|
|
15062
|
+
"+faststart",
|
|
15063
|
+
target
|
|
15064
|
+
], { stdio: "pipe", maxBuffer: 20 * 1024 * 1024 });
|
|
15065
|
+
if (r.status !== 0) {
|
|
15066
|
+
fail(`recording post-processing failed: ${r.error?.message || r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
15067
|
+
}
|
|
15068
|
+
} finally {
|
|
15069
|
+
rmSync(filterPath, { force: true });
|
|
15070
|
+
rmSync(cursor.path, { force: true });
|
|
15071
|
+
}
|
|
15072
|
+
}
|
|
15073
|
+
|
|
15074
|
+
// src/commands/computer/recording.ts
|
|
15075
|
+
var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
|
|
15076
|
+
var RECORD_PATH_FILE = `${STATE_DIR}/recording-path.txt`;
|
|
15077
|
+
var RECORD_RAW_PATH_FILE = `${STATE_DIR}/recording-raw-path.txt`;
|
|
15078
|
+
var RECORD_STARTED_AT_FILE = `${STATE_DIR}/recording-started-at.txt`;
|
|
15079
|
+
var RECORD_FPS_FILE = `${STATE_DIR}/recording-fps.txt`;
|
|
15080
|
+
var RECORD_DIMENSIONS_FILE = `${STATE_DIR}/recording-dimensions.json`;
|
|
15081
|
+
var RECORD_ACTIONS_FILE = `${STATE_DIR}/recording-actions.jsonl`;
|
|
15082
|
+
function recordingStartedAt() {
|
|
15083
|
+
if (!existsSync2(RECORD_STARTED_AT_FILE)) return null;
|
|
15084
|
+
const startedAt = Number.parseInt(readFileSync(RECORD_STARTED_AT_FILE, "utf8").trim(), 10);
|
|
15085
|
+
return Number.isFinite(startedAt) ? startedAt : null;
|
|
15086
|
+
}
|
|
15087
|
+
function logRecordingAction(action) {
|
|
15088
|
+
const startedAt = recordingStartedAt();
|
|
15089
|
+
if (!startedAt) return;
|
|
15090
|
+
const atMs = Date.now() - startedAt;
|
|
15091
|
+
appendFileSync(RECORD_ACTIONS_FILE, `${JSON.stringify({ ...action, atMs })}
|
|
15092
|
+
`);
|
|
15093
|
+
}
|
|
15094
|
+
function readRecordingDimensions() {
|
|
15095
|
+
if (!existsSync2(RECORD_DIMENSIONS_FILE)) return configuredDesktopDimensions();
|
|
15096
|
+
try {
|
|
15097
|
+
const dimensions = JSON.parse(readFileSync(RECORD_DIMENSIONS_FILE, "utf8"));
|
|
15098
|
+
const width = dimensions?.width;
|
|
15099
|
+
const height = dimensions?.height;
|
|
15100
|
+
if (typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0) {
|
|
15101
|
+
return { width, height };
|
|
15102
|
+
}
|
|
15103
|
+
} catch {
|
|
15104
|
+
}
|
|
15105
|
+
fail("invalid recording dimensions state");
|
|
15106
|
+
}
|
|
15107
|
+
function isRecordingActionType(value) {
|
|
15108
|
+
return value === "click" || value === "drag" || value === "key" || value === "move" || value === "scroll" || value === "type";
|
|
15109
|
+
}
|
|
15110
|
+
function isOptionalNumber(value) {
|
|
15111
|
+
return value === void 0 || typeof value === "number";
|
|
15112
|
+
}
|
|
15113
|
+
function readRecordingActions() {
|
|
15114
|
+
if (!existsSync2(RECORD_ACTIONS_FILE)) return [];
|
|
15115
|
+
return readFileSync(RECORD_ACTIONS_FILE, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
15116
|
+
try {
|
|
15117
|
+
const value = JSON.parse(line);
|
|
15118
|
+
if (typeof value !== "object" || value === null) return [];
|
|
15119
|
+
const type = "type" in value ? value.type : void 0;
|
|
15120
|
+
const atMs = "atMs" in value ? value.atMs : void 0;
|
|
15121
|
+
const x = "x" in value ? value.x : void 0;
|
|
15122
|
+
const y = "y" in value ? value.y : void 0;
|
|
15123
|
+
const toX = "toX" in value ? value.toX : void 0;
|
|
15124
|
+
const toY = "toY" in value ? value.toY : void 0;
|
|
15125
|
+
if (!isRecordingActionType(type) || typeof atMs !== "number" || !Number.isFinite(atMs) || !isOptionalNumber(x) || !isOptionalNumber(y) || !isOptionalNumber(toX) || !isOptionalNumber(toY)) {
|
|
15126
|
+
return [];
|
|
15127
|
+
}
|
|
15128
|
+
return [{ type, atMs, x, y, toX, toY }];
|
|
15129
|
+
} catch {
|
|
15130
|
+
return [];
|
|
15131
|
+
}
|
|
15132
|
+
}).sort((a, b) => a.atMs - b.atMs);
|
|
15133
|
+
}
|
|
15134
|
+
async function computerRecordStartCommand(path6, options) {
|
|
15135
|
+
ensureServicesRunning();
|
|
15136
|
+
if (existsSync2(RECORD_PID_FILE)) {
|
|
15137
|
+
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15138
|
+
if (Number.isFinite(pid)) {
|
|
15139
|
+
let alive = false;
|
|
15140
|
+
try {
|
|
15141
|
+
process.kill(pid, 0);
|
|
15142
|
+
alive = true;
|
|
15143
|
+
} catch {
|
|
15144
|
+
}
|
|
15145
|
+
if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
const target = resolvePath(path6);
|
|
15149
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
15150
|
+
const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
|
|
15151
|
+
const { width, height } = configuredDesktopDimensions();
|
|
15152
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
15153
|
+
const rawTarget = `${target}.raw-${Date.now()}.mp4`;
|
|
15154
|
+
rmSync2(RECORD_ACTIONS_FILE, { force: true });
|
|
15155
|
+
const child = spawn3("ffmpeg", [
|
|
15156
|
+
"-y",
|
|
15157
|
+
"-hide_banner",
|
|
15158
|
+
"-loglevel",
|
|
15159
|
+
"warning",
|
|
15160
|
+
"-f",
|
|
15161
|
+
"x11grab",
|
|
15162
|
+
"-framerate",
|
|
15163
|
+
String(fps),
|
|
15164
|
+
"-video_size",
|
|
15165
|
+
`${width}x${height}`,
|
|
15166
|
+
"-draw_mouse",
|
|
15167
|
+
"0",
|
|
15168
|
+
"-i",
|
|
15169
|
+
DEFAULT_DISPLAY,
|
|
15170
|
+
"-c:v",
|
|
15171
|
+
"libx264",
|
|
15172
|
+
"-preset",
|
|
15173
|
+
"ultrafast",
|
|
15174
|
+
"-tune",
|
|
15175
|
+
"zerolatency",
|
|
15176
|
+
"-crf",
|
|
15177
|
+
"23",
|
|
15178
|
+
"-pix_fmt",
|
|
15179
|
+
"yuv420p",
|
|
15180
|
+
"-movflags",
|
|
15181
|
+
"+faststart+frag_keyframe+empty_moov",
|
|
15182
|
+
rawTarget
|
|
15183
|
+
], { detached: true, stdio: "ignore" });
|
|
15184
|
+
child.unref();
|
|
15185
|
+
if (!child.pid) fail("failed to launch ffmpeg");
|
|
15186
|
+
writeFileSync2(RECORD_PID_FILE, String(child.pid));
|
|
15187
|
+
writeFileSync2(RECORD_PATH_FILE, target);
|
|
15188
|
+
writeFileSync2(RECORD_RAW_PATH_FILE, rawTarget);
|
|
15189
|
+
writeFileSync2(RECORD_STARTED_AT_FILE, String(Date.now()));
|
|
15190
|
+
writeFileSync2(RECORD_FPS_FILE, String(fps));
|
|
15191
|
+
writeFileSync2(RECORD_DIMENSIONS_FILE, JSON.stringify({ width, height }));
|
|
15192
|
+
console.log(target);
|
|
15193
|
+
}
|
|
15194
|
+
async function computerRecordStopCommand() {
|
|
15195
|
+
if (!existsSync2(RECORD_PID_FILE) && !existsSync2(RECORD_PATH_FILE)) fail("no recording in progress");
|
|
15196
|
+
if (existsSync2(RECORD_PID_FILE)) {
|
|
15197
|
+
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15198
|
+
if (!Number.isFinite(pid)) fail("invalid recording pidfile");
|
|
15199
|
+
try {
|
|
15200
|
+
process.kill(pid, "SIGINT");
|
|
15201
|
+
} catch {
|
|
15202
|
+
}
|
|
15203
|
+
for (let i = 0; i < 30; i++) {
|
|
15204
|
+
try {
|
|
15205
|
+
process.kill(pid, 0);
|
|
15206
|
+
} catch {
|
|
15207
|
+
break;
|
|
15208
|
+
}
|
|
15209
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
15210
|
+
}
|
|
15211
|
+
rmSync2(RECORD_PID_FILE, { force: true });
|
|
15212
|
+
}
|
|
15213
|
+
if (existsSync2(RECORD_PATH_FILE)) {
|
|
15214
|
+
const target = readFileSync(RECORD_PATH_FILE, "utf8").trim();
|
|
15215
|
+
const rawPath = existsSync2(RECORD_RAW_PATH_FILE) ? readFileSync(RECORD_RAW_PATH_FILE, "utf8").trim() : target;
|
|
15216
|
+
const fps = existsSync2(RECORD_FPS_FILE) ? parseInt(readFileSync(RECORD_FPS_FILE, "utf8").trim(), 10) : 60;
|
|
15217
|
+
const size = readRecordingDimensions();
|
|
15218
|
+
const actions = readRecordingActions();
|
|
15219
|
+
if (rawPath !== target) {
|
|
15220
|
+
renderRecording(rawPath, target, actions, Number.isFinite(fps) ? fps : 60, size);
|
|
15221
|
+
rmSync2(rawPath, { force: true });
|
|
15222
|
+
}
|
|
15223
|
+
console.log(target);
|
|
15224
|
+
rmSync2(RECORD_PATH_FILE, { force: true });
|
|
15225
|
+
rmSync2(RECORD_RAW_PATH_FILE, { force: true });
|
|
15226
|
+
rmSync2(RECORD_STARTED_AT_FILE, { force: true });
|
|
15227
|
+
rmSync2(RECORD_FPS_FILE, { force: true });
|
|
15228
|
+
rmSync2(RECORD_DIMENSIONS_FILE, { force: true });
|
|
15229
|
+
rmSync2(RECORD_ACTIONS_FILE, { force: true });
|
|
15230
|
+
}
|
|
15231
|
+
}
|
|
15232
|
+
|
|
15233
|
+
// src/commands/computer/index.ts
|
|
14566
15234
|
function desktopBridgeStatus() {
|
|
14567
|
-
const r =
|
|
15235
|
+
const r = spawnSync3("bash", [SERVICES_SCRIPT, "--status-json"], { stdio: "pipe" });
|
|
14568
15236
|
if (r.status !== 0) return null;
|
|
14569
15237
|
try {
|
|
14570
15238
|
return JSON.parse(r.stdout.toString());
|
|
@@ -14581,7 +15249,6 @@ function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
|
|
|
14581
15249
|
if (pids.length > 0) parts.push(`pid${pids.length === 1 ? "" : "s"} ${pids.join(",")}`);
|
|
14582
15250
|
return ` (${parts.join(", ")})`;
|
|
14583
15251
|
}
|
|
14584
|
-
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
14585
15252
|
async function lookupDesktopViewerUrl() {
|
|
14586
15253
|
try {
|
|
14587
15254
|
const list = await listAgentPreviews();
|
|
@@ -14615,7 +15282,7 @@ async function computerStatusCommand() {
|
|
|
14615
15282
|
const bridge = desktopBridgeStatus();
|
|
14616
15283
|
const procs = ["Xvfb", "openbox", "tint2", "x11vnc", "websockify"];
|
|
14617
15284
|
for (const p of procs) {
|
|
14618
|
-
const r =
|
|
15285
|
+
const r = spawnSync3("pgrep", ["-af", p], { stdio: "pipe" });
|
|
14619
15286
|
const running = r.status === 0 && !!r.stdout?.toString().trim();
|
|
14620
15287
|
const suffix = p === "x11vnc" && bridge ? bridgeStatus(bridge.x11vnc, true) : p === "websockify" && bridge ? bridgeStatus(bridge.websockify, false, true) : "";
|
|
14621
15288
|
console.log(` ${running ? chalk20.green("\u25CF") : chalk20.red("\u25CB")} ${p}${suffix}`);
|
|
@@ -14647,12 +15314,12 @@ function brandSvgPath() {
|
|
|
14647
15314
|
}
|
|
14648
15315
|
function loadBrandSvg(canvasW, canvasH) {
|
|
14649
15316
|
const path6 = brandSvgPath();
|
|
14650
|
-
if (!
|
|
15317
|
+
if (!existsSync3(path6)) {
|
|
14651
15318
|
fail(
|
|
14652
15319
|
`Brand wallpaper SVG missing at ${path6}. The workspace image is out of date \u2014 \`desktop/brand-wallpaper.svg\` must be installed at $REPLICAS_DESKTOP_TEMPLATES.`
|
|
14653
15320
|
);
|
|
14654
15321
|
}
|
|
14655
|
-
return
|
|
15322
|
+
return readFileSync2(path6, "utf8").replace(/(<svg[^>]*\s)width="\d+"/, `$1width="${canvasW}"`).replace(/(<svg[^>]*\s)height="\d+"/, `$1height="${canvasH}"`);
|
|
14656
15323
|
}
|
|
14657
15324
|
var BRAND_PAD_FRACTION = 0.06;
|
|
14658
15325
|
var SCREENSHOT_CORNER_FRACTION = 0.022;
|
|
@@ -14687,8 +15354,8 @@ ${labels.join("\n")}
|
|
|
14687
15354
|
</svg>`;
|
|
14688
15355
|
}
|
|
14689
15356
|
function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
|
|
14690
|
-
|
|
14691
|
-
const r =
|
|
15357
|
+
writeFileSync3(gridPath, buildGridSvg(width, height, gridSize));
|
|
15358
|
+
const r = spawnSync3(
|
|
14692
15359
|
"ffmpeg",
|
|
14693
15360
|
[
|
|
14694
15361
|
"-y",
|
|
@@ -14715,7 +15382,7 @@ function overlayGrid(rawPath, target, width, height, gridSize, gridPath) {
|
|
|
14715
15382
|
}
|
|
14716
15383
|
async function computerScreenshotCommand(path6, options = {}) {
|
|
14717
15384
|
const target = resolvePath(path6);
|
|
14718
|
-
|
|
15385
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
14719
15386
|
const stamp = `${process.pid}-${Date.now()}`;
|
|
14720
15387
|
const rawPath = `/tmp/replicas-screenshot-${stamp}.raw.png`;
|
|
14721
15388
|
const svgPath = `/tmp/replicas-screenshot-${stamp}.brand.svg`;
|
|
@@ -14727,7 +15394,7 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14727
15394
|
const { width, height } = readPngDimensions(rawPath);
|
|
14728
15395
|
const gridSize = parseGridSize(options.grid);
|
|
14729
15396
|
if (options.raw && gridSize === null) {
|
|
14730
|
-
|
|
15397
|
+
copyFileSync2(rawPath, target);
|
|
14731
15398
|
console.log(`${target} (${width}x${height}, raw 1:1 desktop pixels)`);
|
|
14732
15399
|
return;
|
|
14733
15400
|
}
|
|
@@ -14747,16 +15414,16 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14747
15414
|
const shadowMargin = shadowSigma * 3;
|
|
14748
15415
|
const shadowW = width + shadowMargin * 2;
|
|
14749
15416
|
const shadowH = height + shadowMargin * 2;
|
|
14750
|
-
|
|
14751
|
-
|
|
15417
|
+
writeFileSync3(svgPath, loadBrandSvg(canvasW, canvasH));
|
|
15418
|
+
writeFileSync3(
|
|
14752
15419
|
maskPath,
|
|
14753
15420
|
SCREENSHOT_MASK_TEMPLATE.replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
14754
15421
|
);
|
|
14755
|
-
|
|
15422
|
+
writeFileSync3(
|
|
14756
15423
|
shadowPath,
|
|
14757
15424
|
SHADOW_MASK_TEMPLATE.replace(/__SW__/g, String(shadowW)).replace(/__SH__/g, String(shadowH)).replace(/__M__/g, String(shadowMargin)).replace(/__W__/g, String(width)).replace(/__H__/g, String(height)).replace(/__R__/g, String(cornerR))
|
|
14758
15425
|
);
|
|
14759
|
-
const r =
|
|
15426
|
+
const r = spawnSync3(
|
|
14760
15427
|
"ffmpeg",
|
|
14761
15428
|
[
|
|
14762
15429
|
"-y",
|
|
@@ -14785,16 +15452,16 @@ async function computerScreenshotCommand(path6, options = {}) {
|
|
|
14785
15452
|
fail(`ffmpeg branding failed: ${r.stderr?.toString().trim() || `exit ${r.status}`}`);
|
|
14786
15453
|
}
|
|
14787
15454
|
} finally {
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
15455
|
+
rmSync3(rawPath, { force: true });
|
|
15456
|
+
rmSync3(svgPath, { force: true });
|
|
15457
|
+
rmSync3(maskPath, { force: true });
|
|
15458
|
+
rmSync3(shadowPath, { force: true });
|
|
15459
|
+
rmSync3(gridPath, { force: true });
|
|
14793
15460
|
}
|
|
14794
15461
|
console.log(target);
|
|
14795
15462
|
}
|
|
14796
15463
|
function hashFile(path6) {
|
|
14797
|
-
return createHash2("sha256").update(
|
|
15464
|
+
return createHash2("sha256").update(readFileSync2(path6)).digest("hex");
|
|
14798
15465
|
}
|
|
14799
15466
|
async function captureStableRawScreenshot(target, options) {
|
|
14800
15467
|
const start = Date.now();
|
|
@@ -14821,7 +15488,7 @@ async function captureStableRawScreenshot(target, options) {
|
|
|
14821
15488
|
lastChangeAt = now;
|
|
14822
15489
|
}
|
|
14823
15490
|
lastHash = hash;
|
|
14824
|
-
|
|
15491
|
+
copyFileSync2(framePath, target);
|
|
14825
15492
|
if (frames > 1 && now - lastChangeAt >= options.stableMs) {
|
|
14826
15493
|
return { width, height, stable: true, elapsedMs: now - start, frames, changes };
|
|
14827
15494
|
}
|
|
@@ -14829,26 +15496,9 @@ async function captureStableRawScreenshot(target, options) {
|
|
|
14829
15496
|
}
|
|
14830
15497
|
return { width, height, stable: false, elapsedMs: Date.now() - start, frames, changes };
|
|
14831
15498
|
} finally {
|
|
14832
|
-
|
|
15499
|
+
rmSync3(framePath, { force: true });
|
|
14833
15500
|
}
|
|
14834
15501
|
}
|
|
14835
|
-
function getMouseLocation() {
|
|
14836
|
-
const out = tryDisplayCmd("xdotool", ["getmouselocation", "--shell"]);
|
|
14837
|
-
if (!out) return null;
|
|
14838
|
-
const values = Object.fromEntries(
|
|
14839
|
-
out.split("\n").map((line) => {
|
|
14840
|
-
const [key, value] = line.split("=");
|
|
14841
|
-
return [key.toLowerCase(), Number.parseInt(value, 10)];
|
|
14842
|
-
})
|
|
14843
|
-
);
|
|
14844
|
-
if (!Number.isFinite(values.x) || !Number.isFinite(values.y)) return null;
|
|
14845
|
-
return {
|
|
14846
|
-
x: values.x,
|
|
14847
|
-
y: values.y,
|
|
14848
|
-
screen: Number.isFinite(values.screen) ? values.screen : 0,
|
|
14849
|
-
window: Number.isFinite(values.window) ? values.window : 0
|
|
14850
|
-
};
|
|
14851
|
-
}
|
|
14852
15502
|
function getActiveWindowTitle() {
|
|
14853
15503
|
return tryDisplayCmd("xdotool", ["getactivewindow", "getwindowname"]);
|
|
14854
15504
|
}
|
|
@@ -14856,9 +15506,13 @@ function getVisibleWindowTitles() {
|
|
|
14856
15506
|
const out = tryDisplayCmd("xdotool", ["search", "--onlyvisible", "--name", ".", "getwindowname", "%@"]);
|
|
14857
15507
|
return out ? out.split("\n").filter(Boolean).slice(0, 20) : [];
|
|
14858
15508
|
}
|
|
15509
|
+
function recordingMousePosition() {
|
|
15510
|
+
const mouse = getMouseLocation();
|
|
15511
|
+
return mouse ? { x: mouse.x, y: mouse.y } : null;
|
|
15512
|
+
}
|
|
14859
15513
|
async function computerObserveCommand(path6, options = {}) {
|
|
14860
15514
|
const target = resolvePath(path6);
|
|
14861
|
-
|
|
15515
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
14862
15516
|
const timeoutMs = options.timeout ? parseCoord(options.timeout, "--timeout") : 3e3;
|
|
14863
15517
|
const stableMs = options.stableMs ? parseCoord(options.stableMs, "--stable-ms") : 600;
|
|
14864
15518
|
const pollMs = options.pollMs ? parseCoord(options.pollMs, "--poll-ms") : 200;
|
|
@@ -14872,7 +15526,7 @@ async function computerObserveCommand(path6, options = {}) {
|
|
|
14872
15526
|
const capture = await captureStableRawScreenshot(rawPath, { timeoutMs, stableMs, pollMs });
|
|
14873
15527
|
const gridSize = options.raw ? null : parseGridSize(options.grid ?? true);
|
|
14874
15528
|
if (gridSize === null) {
|
|
14875
|
-
|
|
15529
|
+
copyFileSync2(rawPath, target);
|
|
14876
15530
|
} else {
|
|
14877
15531
|
overlayGrid(rawPath, target, capture.width, capture.height, gridSize, gridPath);
|
|
14878
15532
|
}
|
|
@@ -14891,8 +15545,8 @@ async function computerObserveCommand(path6, options = {}) {
|
|
|
14891
15545
|
gridSize
|
|
14892
15546
|
}, null, 2));
|
|
14893
15547
|
} finally {
|
|
14894
|
-
|
|
14895
|
-
|
|
15548
|
+
rmSync3(rawPath, { force: true });
|
|
15549
|
+
rmSync3(gridPath, { force: true });
|
|
14896
15550
|
}
|
|
14897
15551
|
}
|
|
14898
15552
|
async function fetchChromeJson(path6) {
|
|
@@ -15018,7 +15672,7 @@ async function evaluateChromeTarget(webSocketDebuggerUrl, expression) {
|
|
|
15018
15672
|
}));
|
|
15019
15673
|
});
|
|
15020
15674
|
ws.addEventListener("message", (event) => {
|
|
15021
|
-
const data = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf8");
|
|
15675
|
+
const data = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : Buffer.from(event.data).toString("utf8");
|
|
15022
15676
|
let parsed;
|
|
15023
15677
|
try {
|
|
15024
15678
|
parsed = JSON.parse(data);
|
|
@@ -15343,6 +15997,7 @@ async function computerClickCommand(xStr, yStr, options) {
|
|
|
15343
15997
|
}
|
|
15344
15998
|
}
|
|
15345
15999
|
runDisplayCmd("xdotool", args);
|
|
16000
|
+
logRecordingAction({ type: "click", x, y });
|
|
15346
16001
|
console.log(`clicked ${button === "1" ? "left" : button === "2" ? "middle" : button === "3" ? "right" : `button ${button}`} at (${x},${y})${options.double ? " x2" : ""}`);
|
|
15347
16002
|
}
|
|
15348
16003
|
async function computerMoveCommand(xStr, yStr) {
|
|
@@ -15350,15 +16005,20 @@ async function computerMoveCommand(xStr, yStr) {
|
|
|
15350
16005
|
const x = parseScreenCoord(xStr, "x", dimensions.width);
|
|
15351
16006
|
const y = parseScreenCoord(yStr, "y", dimensions.height);
|
|
15352
16007
|
runDisplayCmd("xdotool", ["mousemove", "--sync", String(x), String(y)]);
|
|
16008
|
+
logRecordingAction({ type: "move", x, y });
|
|
15353
16009
|
console.log(`moved to (${x},${y})`);
|
|
15354
16010
|
}
|
|
15355
16011
|
async function computerTypeCommand(text, options) {
|
|
15356
16012
|
const delay = options.delay ? parseCoord(options.delay, "--delay") : 12;
|
|
15357
16013
|
runDisplayCmd("xdotool", ["type", "--delay", String(delay), "--", text]);
|
|
16014
|
+
const position = recordingMousePosition();
|
|
16015
|
+
logRecordingAction({ type: "type", ...position ?? {} });
|
|
15358
16016
|
console.log(`typed ${text.length} char${text.length === 1 ? "" : "s"}`);
|
|
15359
16017
|
}
|
|
15360
16018
|
async function computerKeyCommand(combo) {
|
|
15361
16019
|
runDisplayCmd("xdotool", ["key", "--", combo]);
|
|
16020
|
+
const position = recordingMousePosition();
|
|
16021
|
+
logRecordingAction({ type: "key", ...position ?? {} });
|
|
15362
16022
|
console.log(`pressed ${combo}`);
|
|
15363
16023
|
}
|
|
15364
16024
|
async function computerScrollCommand(direction, options) {
|
|
@@ -15368,17 +16028,24 @@ async function computerScrollCommand(direction, options) {
|
|
|
15368
16028
|
if (!button) fail(`direction must be one of up|down|left|right (got "${direction}")`);
|
|
15369
16029
|
const amount = options.amount ? parseCoord(options.amount, "--amount") : 3;
|
|
15370
16030
|
const args = [];
|
|
16031
|
+
let hoverPosition = null;
|
|
15371
16032
|
if (options.x && options.y) {
|
|
15372
16033
|
const dimensions = getDisplayDimensions();
|
|
16034
|
+
hoverPosition = {
|
|
16035
|
+
x: parseScreenCoord(options.x, "--x", dimensions.width),
|
|
16036
|
+
y: parseScreenCoord(options.y, "--y", dimensions.height)
|
|
16037
|
+
};
|
|
15373
16038
|
args.push(
|
|
15374
16039
|
"mousemove",
|
|
15375
16040
|
"--sync",
|
|
15376
|
-
String(
|
|
15377
|
-
String(
|
|
16041
|
+
String(hoverPosition.x),
|
|
16042
|
+
String(hoverPosition.y)
|
|
15378
16043
|
);
|
|
15379
16044
|
}
|
|
15380
16045
|
args.push("click", "--repeat", String(amount), "--delay", "30", button);
|
|
15381
16046
|
runDisplayCmd("xdotool", args);
|
|
16047
|
+
const position = hoverPosition ?? recordingMousePosition();
|
|
16048
|
+
logRecordingAction({ type: "scroll", ...position ?? {} });
|
|
15382
16049
|
console.log(`scrolled ${dir} x${amount}`);
|
|
15383
16050
|
}
|
|
15384
16051
|
async function computerDragCommand(fx, fy, tx, ty) {
|
|
@@ -15387,20 +16054,24 @@ async function computerDragCommand(fx, fy, tx, ty) {
|
|
|
15387
16054
|
const fromY = parseScreenCoord(fy, "fromY", dimensions.height);
|
|
15388
16055
|
const toX = parseScreenCoord(tx, "toX", dimensions.width);
|
|
15389
16056
|
const toY = parseScreenCoord(ty, "toY", dimensions.height);
|
|
15390
|
-
|
|
16057
|
+
const distance = Math.hypot(toX - fromX, toY - fromY);
|
|
16058
|
+
const steps = clamp(Math.ceil(distance / 70), 8, 24);
|
|
16059
|
+
const args = [
|
|
15391
16060
|
"mousemove",
|
|
15392
16061
|
"--sync",
|
|
15393
16062
|
String(fromX),
|
|
15394
16063
|
String(fromY),
|
|
15395
16064
|
"mousedown",
|
|
15396
|
-
"1",
|
|
15397
|
-
"mousemove",
|
|
15398
|
-
"--sync",
|
|
15399
|
-
String(toX),
|
|
15400
|
-
String(toY),
|
|
15401
|
-
"mouseup",
|
|
15402
16065
|
"1"
|
|
15403
|
-
]
|
|
16066
|
+
];
|
|
16067
|
+
for (let i = 1; i <= steps; i++) {
|
|
16068
|
+
const x = Math.round(fromX + (toX - fromX) * i / steps);
|
|
16069
|
+
const y = Math.round(fromY + (toY - fromY) * i / steps);
|
|
16070
|
+
args.push("mousemove", "--sync", String(x), String(y), "sleep", "0.018");
|
|
16071
|
+
}
|
|
16072
|
+
args.push("mouseup", "1");
|
|
16073
|
+
runDisplayCmd("xdotool", args);
|
|
16074
|
+
logRecordingAction({ type: "drag", x: fromX, y: fromY, toX, toY });
|
|
15404
16075
|
console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
|
|
15405
16076
|
}
|
|
15406
16077
|
var APP_ALIASES = {
|
|
@@ -15423,10 +16094,10 @@ var LEGACY_CHROME_ALIAS = [
|
|
|
15423
16094
|
];
|
|
15424
16095
|
async function computerLaunchCommand(app, args) {
|
|
15425
16096
|
ensureServicesRunning();
|
|
15426
|
-
const baseArgs = app === "chrome" && !
|
|
16097
|
+
const baseArgs = app === "chrome" && !existsSync3(CHROME_WRAPPER) ? LEGACY_CHROME_ALIAS : APP_ALIASES[app] ?? [app];
|
|
15427
16098
|
const bin = baseArgs[0];
|
|
15428
16099
|
const fullArgs = [...baseArgs.slice(1), ...args];
|
|
15429
|
-
const child =
|
|
16100
|
+
const child = spawn4(bin, fullArgs, {
|
|
15430
16101
|
env: withDisplay(),
|
|
15431
16102
|
detached: true,
|
|
15432
16103
|
stdio: "ignore"
|
|
@@ -15434,85 +16105,6 @@ async function computerLaunchCommand(app, args) {
|
|
|
15434
16105
|
child.unref();
|
|
15435
16106
|
console.log(`launched ${bin} (pid ${child.pid})`);
|
|
15436
16107
|
}
|
|
15437
|
-
var RECORD_PID_FILE = `${STATE_DIR}/ffmpeg.pid`;
|
|
15438
|
-
async function computerRecordStartCommand(path6, options) {
|
|
15439
|
-
ensureServicesRunning();
|
|
15440
|
-
if (existsSync(RECORD_PID_FILE)) {
|
|
15441
|
-
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15442
|
-
if (Number.isFinite(pid)) {
|
|
15443
|
-
let alive = false;
|
|
15444
|
-
try {
|
|
15445
|
-
process.kill(pid, 0);
|
|
15446
|
-
alive = true;
|
|
15447
|
-
} catch {
|
|
15448
|
-
}
|
|
15449
|
-
if (alive) fail(`recording already in progress (pid ${pid}). run \`replicas computer record stop\` first.`);
|
|
15450
|
-
}
|
|
15451
|
-
}
|
|
15452
|
-
const target = resolvePath(path6);
|
|
15453
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
15454
|
-
const fps = options.fps ? parseCoord(options.fps, "--fps") : 60;
|
|
15455
|
-
const width = process.env.REPLICAS_DESKTOP_WIDTH || String(DESKTOP_VIEWER_WIDTH);
|
|
15456
|
-
const height = process.env.REPLICAS_DESKTOP_HEIGHT || String(DESKTOP_VIEWER_HEIGHT);
|
|
15457
|
-
const child = spawn3("ffmpeg", [
|
|
15458
|
-
"-y",
|
|
15459
|
-
"-hide_banner",
|
|
15460
|
-
"-loglevel",
|
|
15461
|
-
"warning",
|
|
15462
|
-
"-f",
|
|
15463
|
-
"x11grab",
|
|
15464
|
-
"-framerate",
|
|
15465
|
-
String(fps),
|
|
15466
|
-
"-video_size",
|
|
15467
|
-
`${width}x${height}`,
|
|
15468
|
-
"-draw_mouse",
|
|
15469
|
-
"1",
|
|
15470
|
-
"-i",
|
|
15471
|
-
DEFAULT_DISPLAY,
|
|
15472
|
-
"-c:v",
|
|
15473
|
-
"libx264",
|
|
15474
|
-
"-preset",
|
|
15475
|
-
"ultrafast",
|
|
15476
|
-
"-tune",
|
|
15477
|
-
"zerolatency",
|
|
15478
|
-
"-crf",
|
|
15479
|
-
"23",
|
|
15480
|
-
"-pix_fmt",
|
|
15481
|
-
"yuv420p",
|
|
15482
|
-
"-movflags",
|
|
15483
|
-
"+faststart+frag_keyframe+empty_moov",
|
|
15484
|
-
target
|
|
15485
|
-
], { detached: true, stdio: "ignore" });
|
|
15486
|
-
child.unref();
|
|
15487
|
-
if (!child.pid) fail("failed to launch ffmpeg");
|
|
15488
|
-
mkdirSync(STATE_DIR, { recursive: true });
|
|
15489
|
-
writeFileSync(RECORD_PID_FILE, String(child.pid));
|
|
15490
|
-
writeFileSync(`${STATE_DIR}/recording-path.txt`, target);
|
|
15491
|
-
console.log(target);
|
|
15492
|
-
}
|
|
15493
|
-
async function computerRecordStopCommand() {
|
|
15494
|
-
if (!existsSync(RECORD_PID_FILE)) fail("no recording in progress");
|
|
15495
|
-
const pid = parseInt(readFileSync(RECORD_PID_FILE, "utf8").trim(), 10);
|
|
15496
|
-
if (!Number.isFinite(pid)) fail("invalid recording pidfile");
|
|
15497
|
-
try {
|
|
15498
|
-
process.kill(pid, "SIGINT");
|
|
15499
|
-
} catch {
|
|
15500
|
-
}
|
|
15501
|
-
for (let i = 0; i < 30; i++) {
|
|
15502
|
-
try {
|
|
15503
|
-
process.kill(pid, 0);
|
|
15504
|
-
} catch {
|
|
15505
|
-
break;
|
|
15506
|
-
}
|
|
15507
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
15508
|
-
}
|
|
15509
|
-
rmSync(RECORD_PID_FILE, { force: true });
|
|
15510
|
-
const pathFile = `${STATE_DIR}/recording-path.txt`;
|
|
15511
|
-
if (existsSync(pathFile)) {
|
|
15512
|
-
console.log(readFileSync(pathFile, "utf8").trim());
|
|
15513
|
-
rmSync(pathFile, { force: true });
|
|
15514
|
-
}
|
|
15515
|
-
}
|
|
15516
16108
|
|
|
15517
16109
|
// src/commands/interactive.ts
|
|
15518
16110
|
import chalk21 from "chalk";
|
|
@@ -19404,7 +19996,7 @@ automation.command("get <id>").description("Get automation details by ID").actio
|
|
|
19404
19996
|
process.exit(1);
|
|
19405
19997
|
}
|
|
19406
19998
|
});
|
|
19407
|
-
automation.command("create [name]").description("Create a new automation").option("-p, --prompt <prompt>", "Prompt for the automation").option("-e, --environment <environment>", "Environment name or ID").option("--trigger-cron <schedule>", 'Cron schedule expression (e.g. "0 9 * * 1-5")').option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger (default: UTC)").option("--trigger-github <event>", 'GitHub event (e.g. "pull_request.opened")').option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", 'GitLab event (e.g. "merge_request.opened")').option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--lifecycle <policy>", "Workspace lifecycle:
|
|
19999
|
+
automation.command("create [name]").description("Create a new automation").option("-p, --prompt <prompt>", "Prompt for the automation").option("-e, --environment <environment>", "Environment name or ID").option("--trigger-cron <schedule>", 'Cron schedule expression (e.g. "0 9 * * 1-5")').option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger (default: UTC)").option("--trigger-github <event>", 'GitHub event (e.g. "pull_request.opened")').option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", 'GitLab event (e.g. "merge_request.opened")').option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--lifecycle <policy>", "Workspace lifecycle: archive_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups", "Allow follow-up actions on matching PRs").option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode", "Run automation messages in plan mode").option("--goal-mode", "Set automation messages as Codex goals").option("--fast-mode", "Run automation messages in fast mode").option("--personal", "Create a personal automation owned by the authenticated user").option("--disabled", "Create in disabled state").action(async (name, options) => {
|
|
19408
20000
|
try {
|
|
19409
20001
|
await automationCreateCommand(name, {
|
|
19410
20002
|
...options,
|
|
@@ -19419,7 +20011,7 @@ automation.command("create [name]").description("Create a new automation").optio
|
|
|
19419
20011
|
process.exit(1);
|
|
19420
20012
|
}
|
|
19421
20013
|
});
|
|
19422
|
-
automation.command("edit <id>").description("Edit an existing automation").option("-n, --name <name>", "New name").option("-p, --prompt <prompt>", "New prompt").option("-e, --enabled <enabled>", "Enable or disable (true/false)").option("--trigger-cron <schedule>", "Set cron schedule (replaces existing triggers)").option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger").option("--trigger-github <event>", "Set GitHub event (replaces existing triggers)").option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", "Set GitLab event (replaces existing triggers)").option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--environment <environment>", "Environment name or ID").option("--lifecycle <policy>", "Workspace lifecycle:
|
|
20014
|
+
automation.command("edit <id>").description("Edit an existing automation").option("-n, --name <name>", "New name").option("-p, --prompt <prompt>", "New prompt").option("-e, --enabled <enabled>", "Enable or disable (true/false)").option("--trigger-cron <schedule>", "Set cron schedule (replaces existing triggers)").option("--trigger-cron-timezone <timezone>", "Timezone for cron trigger").option("--trigger-github <event>", "Set GitHub event (replaces existing triggers)").option("--trigger-github-repos <repos>", "Comma-separated repo names to filter GitHub trigger").option("--trigger-gitlab <event>", "Set GitLab event (replaces existing triggers)").option("--trigger-gitlab-repos <repos>", "Comma-separated repo names to filter GitLab trigger").option("--environment <environment>", "Environment name or ID").option("--lifecycle <policy>", "Workspace lifecycle: archive_when_done, sleep_when_done, delete_after_inactivity, default").option("--auto-stop-minutes <minutes>", "Inactivity timeout in minutes (3-1440, requires --lifecycle default or delete_after_inactivity)").option("--pr-followups <enabled>", "Allow follow-up actions on matching PRs (true/false)", parseBooleanOption).option("--agent-provider <provider>", 'Coding agent to use: claude, codex, cursor, relay (or "none" to inherit org default)').option("--model <model>", 'Model identifier (must be valid for --agent-provider; pass "none" to clear)').option("--thinking-level <level>", 'Thinking/reasoning level: low, medium, high, max (or "none" to clear)').option("--plan-mode <enabled>", "Run automation messages in plan mode (true/false)", parseBooleanOption).option("--goal-mode <enabled>", "Set automation messages as Codex goals (true/false)", parseBooleanOption).option("--fast-mode <enabled>", "Run automation messages in fast mode (true/false)", parseBooleanOption).action(async (id, options) => {
|
|
19423
20015
|
try {
|
|
19424
20016
|
await automationEditCommand(id, options);
|
|
19425
20017
|
} catch (error) {
|