@leg3ndy/otto-bridge 0.9.2 → 1.0.0
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/README.md +67 -8
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/cli_terminal.js +490 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +18 -1
- package/dist/main.js +23 -0
- package/dist/runtime.js +91 -13
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
@@ -8,6 +8,9 @@ import { loadManagedBridgeExtensionState, saveManagedBridgeExtensionState, } fro
|
|
|
8
8
|
import { postDeviceJson, uploadDeviceJobArtifact } from "../http.js";
|
|
9
9
|
import { WHATSAPP_WEB_URL, WhatsAppBackgroundBrowser, } from "../whatsapp_background.js";
|
|
10
10
|
import { verifyExpectedWhatsAppMessage } from "../whatsapp_verification.js";
|
|
11
|
+
import { parseJobRuntimeManifest, runtimeExpectedArtifactIdForActionIndex, runtimeStepIdForActionIndex, } from "../runtime_contract.js";
|
|
12
|
+
import { applyStructuredUpdateToText, parseStructuredPatch, } from "../agentic_runtime/patch/structured_patch.js";
|
|
13
|
+
import { assertActionAllowedByWorkspacePolicy, assertCwdInsideWorkspace, assertPathInsideWorkspace, buildWorkspaceMemory, expandUserPathLike, resolveWorkspaceContext, } from "../agentic_runtime/workspace/manager.js";
|
|
11
14
|
const KNOWN_APPS = [
|
|
12
15
|
{ canonical: "Safari", patterns: [/\bsafari\b/i] },
|
|
13
16
|
{ canonical: "Google Chrome", patterns: [/\bgoogle chrome\b/i, /\bchrome\b/i] },
|
|
@@ -47,6 +50,16 @@ const FILE_SEARCH_SKIP_DIRS = new Set([
|
|
|
47
50
|
"Library",
|
|
48
51
|
".Trash",
|
|
49
52
|
]);
|
|
53
|
+
const RUN_TEST_PROFILES = new Set([
|
|
54
|
+
"pytest",
|
|
55
|
+
"npm_test",
|
|
56
|
+
"pnpm_test",
|
|
57
|
+
"yarn_test",
|
|
58
|
+
"bun_test",
|
|
59
|
+
"lint",
|
|
60
|
+
"build",
|
|
61
|
+
]);
|
|
62
|
+
const PACKAGE_MANAGER_PRIORITY = ["pnpm", "yarn", "bun", "npm"];
|
|
50
63
|
const GENERIC_VISUAL_STOP_WORDS = new Set([
|
|
51
64
|
"o",
|
|
52
65
|
"a",
|
|
@@ -675,20 +688,7 @@ function mimeTypeFromPath(filePath) {
|
|
|
675
688
|
return "application/octet-stream";
|
|
676
689
|
}
|
|
677
690
|
function expandUserPath(value) {
|
|
678
|
-
|
|
679
|
-
if (!trimmed) {
|
|
680
|
-
return os.homedir();
|
|
681
|
-
}
|
|
682
|
-
if (trimmed === "~") {
|
|
683
|
-
return os.homedir();
|
|
684
|
-
}
|
|
685
|
-
if (trimmed.startsWith("~/")) {
|
|
686
|
-
return path.join(os.homedir(), trimmed.slice(2));
|
|
687
|
-
}
|
|
688
|
-
if (path.isAbsolute(trimmed)) {
|
|
689
|
-
return trimmed;
|
|
690
|
-
}
|
|
691
|
-
return path.resolve(process.cwd(), trimmed);
|
|
691
|
+
return expandUserPathLike(value, process.cwd());
|
|
692
692
|
}
|
|
693
693
|
function clipText(value, maxLength) {
|
|
694
694
|
if (value.length <= maxLength) {
|
|
@@ -1044,6 +1044,245 @@ function parseStructuredActions(job) {
|
|
|
1044
1044
|
}
|
|
1045
1045
|
continue;
|
|
1046
1046
|
}
|
|
1047
|
+
if (type === "write_json_file" || type === "write_json" || type === "save_json_file" || type === "save_json") {
|
|
1048
|
+
const targetPath = asString(action.path)
|
|
1049
|
+
|| asString(action.destination)
|
|
1050
|
+
|| asString(action.file_path)
|
|
1051
|
+
|| (asString(action.filename) ? "~/Desktop" : "");
|
|
1052
|
+
const data = ("data" in action ? action.data : ("json" in action ? action.json : ("content" in action ? action.content : undefined)));
|
|
1053
|
+
if (targetPath && data !== undefined) {
|
|
1054
|
+
actions.push({
|
|
1055
|
+
type: "write_json_file",
|
|
1056
|
+
path: targetPath,
|
|
1057
|
+
data,
|
|
1058
|
+
filename: asString(action.filename) || asString(action.file_name) || asString(action.name) || undefined,
|
|
1059
|
+
pretty: action.pretty !== false,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (type === "apply_patch" || type === "patch_apply" || type === "edit_patch") {
|
|
1065
|
+
const patch = asString(action.patch) || asString(action.content) || asString(action.diff);
|
|
1066
|
+
const cwd = asString(action.cwd) || asString(action.path);
|
|
1067
|
+
if (patch && cwd) {
|
|
1068
|
+
const targetFiles = Array.isArray(action.target_files)
|
|
1069
|
+
? action.target_files
|
|
1070
|
+
.map((item) => asString(item))
|
|
1071
|
+
.filter((item) => Boolean(item))
|
|
1072
|
+
: [];
|
|
1073
|
+
actions.push({
|
|
1074
|
+
type: "apply_patch",
|
|
1075
|
+
cwd,
|
|
1076
|
+
patch,
|
|
1077
|
+
target_files: targetFiles.length > 0 ? targetFiles : undefined,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (type === "mkdir" || type === "make_directory" || type === "create_directory") {
|
|
1083
|
+
const directoryPath = asString(action.path) || asString(action.destination) || asString(action.directory_path);
|
|
1084
|
+
if (directoryPath) {
|
|
1085
|
+
actions.push({
|
|
1086
|
+
type: "mkdir",
|
|
1087
|
+
path: directoryPath,
|
|
1088
|
+
create_parents: action.create_parents !== false,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (type === "move_file" || type === "move_path" || type === "rename_file") {
|
|
1094
|
+
const sourcePath = asString(action.source_path) || asString(action.source) || asString(action.path);
|
|
1095
|
+
const destinationPath = asString(action.destination_path) || asString(action.destination) || asString(action.target_path) || asString(action.target);
|
|
1096
|
+
if (sourcePath && destinationPath) {
|
|
1097
|
+
actions.push({
|
|
1098
|
+
type: "move_file",
|
|
1099
|
+
source_path: sourcePath,
|
|
1100
|
+
destination_path: destinationPath,
|
|
1101
|
+
overwrite: action.overwrite === true,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (type === "git_clone" || type === "clone_repo" || type === "repo_clone") {
|
|
1107
|
+
const repository = asString(action.repository) || asString(action.repo) || asString(action.url) || asString(action.remote);
|
|
1108
|
+
const destinationPath = asString(action.destination_path) || asString(action.destination) || asString(action.path) || asString(action.target_path);
|
|
1109
|
+
const depth = typeof action.depth === "number" ? Math.max(1, Math.min(1_000, Math.round(action.depth))) : undefined;
|
|
1110
|
+
if (repository && destinationPath) {
|
|
1111
|
+
actions.push({
|
|
1112
|
+
type: "git_clone",
|
|
1113
|
+
repository,
|
|
1114
|
+
destination_path: destinationPath,
|
|
1115
|
+
branch: asString(action.branch) || asString(action.ref) || undefined,
|
|
1116
|
+
depth,
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (type === "git_fetch" || type === "fetch_repo" || type === "repo_fetch") {
|
|
1122
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1123
|
+
if (cwd) {
|
|
1124
|
+
actions.push({
|
|
1125
|
+
type: "git_fetch",
|
|
1126
|
+
cwd,
|
|
1127
|
+
remote: asString(action.remote) || undefined,
|
|
1128
|
+
prune: action.prune === true,
|
|
1129
|
+
tags: action.tags === true,
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (type === "git_checkout" || type === "checkout_branch" || type === "switch_branch") {
|
|
1135
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1136
|
+
const target = asString(action.target) || asString(action.branch) || asString(action.ref) || asString(action.target_ref);
|
|
1137
|
+
if (cwd && target) {
|
|
1138
|
+
actions.push({
|
|
1139
|
+
type: "git_checkout",
|
|
1140
|
+
cwd,
|
|
1141
|
+
target,
|
|
1142
|
+
start_point: asString(action.start_point) || asString(action.startPoint) || asString(action.from_ref) || undefined,
|
|
1143
|
+
create_branch: action.create_branch === true || action.new_branch === true,
|
|
1144
|
+
detach: action.detach === true,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (type === "git_rebase" || type === "rebase_branch") {
|
|
1150
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1151
|
+
const target = asString(action.target) || asString(action.upstream) || asString(action.branch) || asString(action.ref);
|
|
1152
|
+
if (cwd && target) {
|
|
1153
|
+
actions.push({
|
|
1154
|
+
type: "git_rebase",
|
|
1155
|
+
cwd,
|
|
1156
|
+
target,
|
|
1157
|
+
autostash: action.autostash === true,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (type === "git_merge" || type === "merge_branch") {
|
|
1163
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1164
|
+
const target = asString(action.target) || asString(action.branch) || asString(action.ref) || asString(action.source_branch);
|
|
1165
|
+
if (cwd && target) {
|
|
1166
|
+
actions.push({
|
|
1167
|
+
type: "git_merge",
|
|
1168
|
+
cwd,
|
|
1169
|
+
target,
|
|
1170
|
+
ff_only: action.ff_only === true,
|
|
1171
|
+
no_ff: action.no_ff === true,
|
|
1172
|
+
message: asString(action.message) || asString(action.merge_message) || undefined,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (type === "git_tag" || type === "create_tag") {
|
|
1178
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1179
|
+
const name = asString(action.name) || asString(action.tag) || asString(action.tag_name);
|
|
1180
|
+
if (cwd && name) {
|
|
1181
|
+
actions.push({
|
|
1182
|
+
type: "git_tag",
|
|
1183
|
+
cwd,
|
|
1184
|
+
name,
|
|
1185
|
+
target: asString(action.target) || asString(action.ref) || undefined,
|
|
1186
|
+
annotated: action.annotated === true,
|
|
1187
|
+
message: asString(action.message) || asString(action.annotation) || undefined,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
if (type === "git_add" || type === "git_stage" || type === "stage_files" || type === "stage_changes") {
|
|
1193
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1194
|
+
if (cwd) {
|
|
1195
|
+
const rawPaths = Array.isArray(action.paths)
|
|
1196
|
+
? action.paths
|
|
1197
|
+
.map((item) => asString(item))
|
|
1198
|
+
.filter((item) => Boolean(item))
|
|
1199
|
+
: [];
|
|
1200
|
+
const fallbackTargetPath = asString(action.target_path) || asString(action.file_path) || asString(action.pathspec);
|
|
1201
|
+
actions.push({
|
|
1202
|
+
type: "git_add",
|
|
1203
|
+
cwd,
|
|
1204
|
+
all: action.all === true || action.stage_all === true || (rawPaths.length === 0 && !fallbackTargetPath),
|
|
1205
|
+
paths: rawPaths.length > 0 ? rawPaths : (fallbackTargetPath ? [fallbackTargetPath] : undefined),
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (type === "git_commit" || type === "commit_changes" || type === "git_commit_changes") {
|
|
1211
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1212
|
+
const message = asString(action.message) || asString(action.commit_message) || asString(action.summary);
|
|
1213
|
+
if (cwd && message) {
|
|
1214
|
+
actions.push({
|
|
1215
|
+
type: "git_commit",
|
|
1216
|
+
cwd,
|
|
1217
|
+
message,
|
|
1218
|
+
allow_empty: action.allow_empty === true,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (type === "git_push" || type === "push_changes" || type === "git_push_branch") {
|
|
1224
|
+
const cwd = asString(action.cwd) || asString(action.repo_path) || asString(action.repository_path) || asString(action.workspace_path) || asString(action.path);
|
|
1225
|
+
if (cwd) {
|
|
1226
|
+
actions.push({
|
|
1227
|
+
type: "git_push",
|
|
1228
|
+
cwd,
|
|
1229
|
+
remote: asString(action.remote) || undefined,
|
|
1230
|
+
branch: asString(action.branch) || asString(action.ref) || undefined,
|
|
1231
|
+
set_upstream: action.set_upstream === true,
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (type === "git_status" || type === "repo_status") {
|
|
1237
|
+
const cwd = asString(action.cwd) || asString(action.path);
|
|
1238
|
+
if (cwd) {
|
|
1239
|
+
actions.push({
|
|
1240
|
+
type: "git_status",
|
|
1241
|
+
cwd,
|
|
1242
|
+
include_untracked: action.include_untracked !== false,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (type === "git_diff" || type === "repo_diff") {
|
|
1248
|
+
const cwd = asString(action.cwd) || asString(action.path);
|
|
1249
|
+
if (cwd) {
|
|
1250
|
+
const rawPaths = Array.isArray(action.paths)
|
|
1251
|
+
? action.paths
|
|
1252
|
+
.map((item) => asString(item))
|
|
1253
|
+
.filter((item) => Boolean(item))
|
|
1254
|
+
: [];
|
|
1255
|
+
const fallbackTargetPath = asString(action.target_path);
|
|
1256
|
+
actions.push({
|
|
1257
|
+
type: "git_diff",
|
|
1258
|
+
cwd,
|
|
1259
|
+
staged: action.staged === true || action.cached === true,
|
|
1260
|
+
base_ref: asString(action.base_ref) || asString(action.base) || undefined,
|
|
1261
|
+
paths: rawPaths.length > 0 ? rawPaths : (fallbackTargetPath ? [fallbackTargetPath] : undefined),
|
|
1262
|
+
max_chars: typeof action.max_chars === "number" ? Math.max(1_000, Math.min(20_000, action.max_chars)) : undefined,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (type === "run_tests" || type === "tests" || type === "test_command") {
|
|
1268
|
+
const command = asString(action.command) || asString(action.cmd);
|
|
1269
|
+
const profileCandidate = asString(action.profile) || asString(action.preset) || "";
|
|
1270
|
+
const normalizedProfile = profileCandidate.toLowerCase().replace(/[-\s]+/g, "_");
|
|
1271
|
+
const profile = RUN_TEST_PROFILES.has(normalizedProfile) ? normalizedProfile : undefined;
|
|
1272
|
+
const cwd = asString(action.cwd) || asString(action.path);
|
|
1273
|
+
if ((command || profile) && cwd) {
|
|
1274
|
+
actions.push({
|
|
1275
|
+
type: "run_tests",
|
|
1276
|
+
command: command || undefined,
|
|
1277
|
+
profile,
|
|
1278
|
+
cwd,
|
|
1279
|
+
timeout_seconds: typeof action.timeout_seconds === "number"
|
|
1280
|
+
? Math.max(5, Math.min(1_800, Math.round(action.timeout_seconds)))
|
|
1281
|
+
: undefined,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1047
1286
|
if (type === "list_files" || type === "ls") {
|
|
1048
1287
|
const filePath = asString(action.path) || "~";
|
|
1049
1288
|
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(5_000, action.limit)) : undefined;
|
|
@@ -1080,6 +1319,13 @@ function parseStructuredActions(job) {
|
|
|
1080
1319
|
}
|
|
1081
1320
|
continue;
|
|
1082
1321
|
}
|
|
1322
|
+
if (type === "delete_file" || type === "remove_file") {
|
|
1323
|
+
const filePath = asString(action.path) || asString(action.target_path);
|
|
1324
|
+
if (filePath) {
|
|
1325
|
+
actions.push({ type: "delete_file", path: filePath });
|
|
1326
|
+
}
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1083
1329
|
if (type === "set_volume" || type === "volume") {
|
|
1084
1330
|
const rawLevel = Number(action.level);
|
|
1085
1331
|
if (Number.isFinite(rawLevel)) {
|
|
@@ -1249,6 +1495,46 @@ function extractActions(job) {
|
|
|
1249
1495
|
}
|
|
1250
1496
|
return deriveActionsFromText(job);
|
|
1251
1497
|
}
|
|
1498
|
+
function bindStepReporter(reporter, stepId) {
|
|
1499
|
+
const normalizedStepId = String(stepId || "").trim() || undefined;
|
|
1500
|
+
const withStepId = (options) => ({
|
|
1501
|
+
...(options || {}),
|
|
1502
|
+
stepId: String(options?.stepId || normalizedStepId || "").trim() || undefined,
|
|
1503
|
+
});
|
|
1504
|
+
return {
|
|
1505
|
+
accepted: (options) => reporter.accepted(withStepId(options)),
|
|
1506
|
+
progress: (progressPercent, progressMessage, options) => reporter.progress(progressPercent, progressMessage, withStepId(options)),
|
|
1507
|
+
confirmRequired: (progressMessage, confirmationContext, options) => reporter.confirmRequired(progressMessage, confirmationContext, withStepId(options)),
|
|
1508
|
+
completed: (result, options) => reporter.completed(result, withStepId(options)),
|
|
1509
|
+
failed: (errorMessage, result, options) => reporter.failed(errorMessage, result, withStepId(options)),
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function appendInlineRuntimeArtifact(artifacts, runtimeManifest, actionIndex, kind, payload, stepId) {
|
|
1513
|
+
const artifactId = runtimeExpectedArtifactIdForActionIndex(runtimeManifest, actionIndex, kind);
|
|
1514
|
+
if (!artifactId) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const metadata = asRecord(payload.metadata);
|
|
1518
|
+
const artifact = {
|
|
1519
|
+
...payload,
|
|
1520
|
+
id: String(payload.id || artifactId).trim() || artifactId,
|
|
1521
|
+
artifact_id: artifactId,
|
|
1522
|
+
kind,
|
|
1523
|
+
metadata: {
|
|
1524
|
+
...metadata,
|
|
1525
|
+
expected_artifact_id: artifactId,
|
|
1526
|
+
inline: true,
|
|
1527
|
+
action_index: actionIndex,
|
|
1528
|
+
step_id: String(stepId || "").trim() || undefined,
|
|
1529
|
+
},
|
|
1530
|
+
};
|
|
1531
|
+
const existingIndex = artifacts.findIndex((item) => (String(item.artifact_id || item.id || "").trim() === artifactId));
|
|
1532
|
+
if (existingIndex >= 0) {
|
|
1533
|
+
artifacts[existingIndex] = artifact;
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
artifacts.push(artifact);
|
|
1537
|
+
}
|
|
1252
1538
|
export class NativeMacOSJobExecutor {
|
|
1253
1539
|
bridgeConfig;
|
|
1254
1540
|
cancelledJobs = new Set();
|
|
@@ -1273,6 +1559,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1273
1559
|
throw new Error("The native-macos executor only runs on macOS");
|
|
1274
1560
|
}
|
|
1275
1561
|
const actions = extractActions(job);
|
|
1562
|
+
const runtimeManifest = parseJobRuntimeManifest(job);
|
|
1563
|
+
const stepWorkerIdById = new Map(runtimeManifest.steps
|
|
1564
|
+
.filter((step) => step.step_id)
|
|
1565
|
+
.map((step) => [step.step_id, step.worker_id || undefined]));
|
|
1566
|
+
const workspaceContext = await resolveWorkspaceContext({
|
|
1567
|
+
workspaceContext: runtimeManifest.workspaceContext,
|
|
1568
|
+
actions,
|
|
1569
|
+
});
|
|
1276
1570
|
if (actions.length === 0) {
|
|
1277
1571
|
throw new Error("Otto Bridge native-macos could not derive a supported local action from this request");
|
|
1278
1572
|
}
|
|
@@ -1285,39 +1579,307 @@ export class NativeMacOSJobExecutor {
|
|
|
1285
1579
|
const decision = await reporter.confirmRequired(confirmation.message, {
|
|
1286
1580
|
actions,
|
|
1287
1581
|
executor: "native-macos",
|
|
1582
|
+
}, {
|
|
1583
|
+
stepId: runtimeManifest.approvalStepId,
|
|
1288
1584
|
});
|
|
1289
1585
|
if (decision.action !== "approve") {
|
|
1290
1586
|
throw new JobCancelledError(job.job_id);
|
|
1291
1587
|
}
|
|
1292
1588
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1589
|
+
const completionNotes = [];
|
|
1590
|
+
const artifacts = [];
|
|
1591
|
+
const hookTrace = [];
|
|
1592
|
+
const resultPayload = {
|
|
1593
|
+
executor: "native-macos",
|
|
1594
|
+
actions,
|
|
1595
|
+
artifacts,
|
|
1596
|
+
action_summaries: completionNotes,
|
|
1597
|
+
runtime_hook_trace: hookTrace,
|
|
1598
|
+
};
|
|
1599
|
+
if (runtimeManifest.commandPacks.length > 0) {
|
|
1600
|
+
resultPayload.command_packs = runtimeManifest.commandPacks;
|
|
1601
|
+
artifacts.push({
|
|
1602
|
+
id: `command_packs.${job.job_id}`,
|
|
1603
|
+
kind: "command_packs",
|
|
1604
|
+
summary: `Command packs declarados para este job: ${runtimeManifest.commandPacks.map((item) => item.pack_id).join(", ")}.`,
|
|
1605
|
+
metadata: {
|
|
1606
|
+
pack_count: runtimeManifest.commandPacks.length,
|
|
1607
|
+
selected_pack_ids: runtimeManifest.commandPacks.filter((item) => item.selected === true).map((item) => item.pack_id),
|
|
1608
|
+
},
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
if (runtimeManifest.workerProfiles.length > 0) {
|
|
1612
|
+
resultPayload.worker_profiles = runtimeManifest.workerProfiles;
|
|
1613
|
+
artifacts.push({
|
|
1614
|
+
id: `worker_profiles.${job.job_id}`,
|
|
1615
|
+
kind: "worker_profiles",
|
|
1616
|
+
summary: `Perfis declarados para este job: ${runtimeManifest.workerProfiles.map((item) => item.worker_id).join(", ")}.`,
|
|
1617
|
+
metadata: {
|
|
1618
|
+
worker_count: runtimeManifest.workerProfiles.length,
|
|
1619
|
+
worker_ids: runtimeManifest.workerProfiles.map((item) => item.worker_id),
|
|
1620
|
+
},
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
if (runtimeManifest.workerExecution.length > 0) {
|
|
1624
|
+
resultPayload.worker_execution = runtimeManifest.workerExecution;
|
|
1625
|
+
artifacts.push({
|
|
1626
|
+
id: `worker_execution.${job.job_id}`,
|
|
1627
|
+
kind: "worker_execution",
|
|
1628
|
+
summary: `Policies declaradas para ${runtimeManifest.workerExecution.length} workers deste job.`,
|
|
1629
|
+
metadata: {
|
|
1630
|
+
worker_count: runtimeManifest.workerExecution.length,
|
|
1631
|
+
worker_ids: runtimeManifest.workerExecution.map((item) => item.worker_id),
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
if (runtimeManifest.instructionUpdate) {
|
|
1636
|
+
resultPayload.instruction_update = runtimeManifest.instructionUpdate;
|
|
1637
|
+
artifacts.push({
|
|
1638
|
+
id: `instruction_update.${job.job_id}`,
|
|
1639
|
+
kind: "instruction_update",
|
|
1640
|
+
summary: runtimeManifest.instructionUpdate.reason || "Instruction updater declarado para este job.",
|
|
1641
|
+
metadata: {
|
|
1642
|
+
updater_id: runtimeManifest.instructionUpdate.updater_id,
|
|
1643
|
+
selected: runtimeManifest.instructionUpdate.selected === true,
|
|
1644
|
+
target_count: runtimeManifest.instructionUpdate.target_paths.length,
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
if (runtimeManifest.validationLadder) {
|
|
1649
|
+
resultPayload.validation_ladder = runtimeManifest.validationLadder;
|
|
1650
|
+
artifacts.push({
|
|
1651
|
+
id: `validation_ladder.${job.job_id}`,
|
|
1652
|
+
kind: "validation_ladder",
|
|
1653
|
+
summary: runtimeManifest.validationLadder.summary || "Validation ladder declarada para este job.",
|
|
1654
|
+
metadata: {
|
|
1655
|
+
ladder_id: runtimeManifest.validationLadder.ladder_id,
|
|
1656
|
+
stage_count: runtimeManifest.validationLadder.stages.length,
|
|
1657
|
+
selected_command_pack_id: runtimeManifest.validationLadder.selected_command_pack_id || undefined,
|
|
1658
|
+
},
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
if (runtimeManifest.hookBus) {
|
|
1662
|
+
resultPayload.hook_bus = runtimeManifest.hookBus;
|
|
1663
|
+
artifacts.push({
|
|
1664
|
+
id: `hook_bus.${job.job_id}`,
|
|
1665
|
+
kind: "hook_bus",
|
|
1666
|
+
summary: runtimeManifest.hookBus.summary || "Hook bus declarado para este job.",
|
|
1667
|
+
metadata: {
|
|
1668
|
+
event_count: runtimeManifest.hookBus.events.length,
|
|
1669
|
+
sink_count: runtimeManifest.hookBus.sinks.length,
|
|
1670
|
+
},
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
if (workspaceContext) {
|
|
1674
|
+
resultPayload.workspace_context = {
|
|
1675
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1676
|
+
repo_root: workspaceContext.repoRoot || undefined,
|
|
1677
|
+
default_cwd: workspaceContext.defaultCwd,
|
|
1678
|
+
summary: workspaceContext.summary,
|
|
1679
|
+
roots: workspaceContext.roots,
|
|
1680
|
+
targets: workspaceContext.targets,
|
|
1681
|
+
workspace_policy: workspaceContext.workspacePolicy || undefined,
|
|
1682
|
+
};
|
|
1683
|
+
artifacts.push({
|
|
1684
|
+
id: `workspace_context.${workspaceContext.workspaceId}`,
|
|
1685
|
+
kind: "workspace_context",
|
|
1686
|
+
summary: workspaceContext.summary,
|
|
1687
|
+
metadata: {
|
|
1688
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1689
|
+
repo_root: workspaceContext.repoRoot || undefined,
|
|
1690
|
+
root_count: workspaceContext.roots.length,
|
|
1691
|
+
target_count: workspaceContext.targets.length,
|
|
1692
|
+
policy_profile_id: workspaceContext.workspacePolicy?.profile_id || undefined,
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
if (workspaceContext.instructionBundle) {
|
|
1696
|
+
resultPayload.instruction_bundle = workspaceContext.instructionBundle;
|
|
1697
|
+
artifacts.push({
|
|
1698
|
+
id: `instruction_bundle.${workspaceContext.workspaceId}`,
|
|
1699
|
+
kind: "instruction_bundle",
|
|
1700
|
+
summary: workspaceContext.instructionBundle.summary,
|
|
1701
|
+
metadata: {
|
|
1702
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1703
|
+
source_count: workspaceContext.instructionBundle.source_count,
|
|
1704
|
+
digest: workspaceContext.instructionBundle.digest,
|
|
1705
|
+
},
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
if (workspaceContext.repoManifest) {
|
|
1709
|
+
resultPayload.repo_manifest = workspaceContext.repoManifest;
|
|
1710
|
+
artifacts.push({
|
|
1711
|
+
id: `repo_manifest.${workspaceContext.workspaceId}`,
|
|
1712
|
+
kind: "repo_manifest",
|
|
1713
|
+
summary: workspaceContext.repoManifest.summary,
|
|
1714
|
+
metadata: {
|
|
1715
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1716
|
+
scoped_root_path: workspaceContext.repoManifest.scoped_root_path,
|
|
1717
|
+
detected_repo_root: workspaceContext.repoManifest.detected_repo_root || undefined,
|
|
1718
|
+
repo_root_within_workspace: workspaceContext.repoManifest.repo_root_within_workspace,
|
|
1719
|
+
repo_name: workspaceContext.repoManifest.repo_name,
|
|
1720
|
+
vcs: workspaceContext.repoManifest.vcs,
|
|
1721
|
+
manifest_file_count: workspaceContext.repoManifest.manifest_files.length,
|
|
1722
|
+
lockfile_count: workspaceContext.repoManifest.lockfiles.length,
|
|
1723
|
+
package_managers: workspaceContext.repoManifest.package_managers,
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
if (workspaceContext.workspaceIndex) {
|
|
1728
|
+
resultPayload.workspace_index = workspaceContext.workspaceIndex;
|
|
1729
|
+
artifacts.push({
|
|
1730
|
+
id: `workspace_index.${workspaceContext.workspaceId}`,
|
|
1731
|
+
kind: "workspace_index",
|
|
1732
|
+
summary: workspaceContext.workspaceIndex.summary,
|
|
1733
|
+
metadata: {
|
|
1734
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1735
|
+
base_path: workspaceContext.workspaceIndex.base_path,
|
|
1736
|
+
scanned_directory_count: workspaceContext.workspaceIndex.scanned_directory_count,
|
|
1737
|
+
scanned_file_count: workspaceContext.workspaceIndex.scanned_file_count,
|
|
1738
|
+
truncated: workspaceContext.workspaceIndex.truncated,
|
|
1739
|
+
key_file_count: workspaceContext.workspaceIndex.key_files.length,
|
|
1740
|
+
},
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
if (workspaceContext.workspacePolicy) {
|
|
1744
|
+
resultPayload.workspace_policy = workspaceContext.workspacePolicy;
|
|
1745
|
+
artifacts.push({
|
|
1746
|
+
id: `workspace_policy.${workspaceContext.workspaceId}`,
|
|
1747
|
+
kind: "workspace_policy",
|
|
1748
|
+
summary: workspaceContext.workspacePolicy.summary,
|
|
1749
|
+
metadata: {
|
|
1750
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1751
|
+
profile_id: workspaceContext.workspacePolicy.profile_id,
|
|
1752
|
+
allow_shell: workspaceContext.workspacePolicy.allow_shell,
|
|
1753
|
+
allow_code_write: workspaceContext.workspacePolicy.allow_code_write,
|
|
1754
|
+
allow_destructive: workspaceContext.workspacePolicy.allow_destructive,
|
|
1755
|
+
allow_release: workspaceContext.workspacePolicy.allow_release,
|
|
1756
|
+
blocked_action_count: workspaceContext.workspacePolicy.blocked_action_types.length,
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
const attachWorkspaceMemory = (jobStatus) => {
|
|
1762
|
+
if (!workspaceContext) {
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const workspaceMemory = buildWorkspaceMemory({
|
|
1766
|
+
workspaceId: workspaceContext.workspaceId,
|
|
1767
|
+
workspace: workspaceContext,
|
|
1768
|
+
priorMemory: workspaceContext.workspaceMemory,
|
|
1298
1769
|
actions,
|
|
1299
1770
|
artifacts,
|
|
1300
|
-
|
|
1301
|
-
|
|
1771
|
+
jobId: job.job_id,
|
|
1772
|
+
jobStatus,
|
|
1773
|
+
});
|
|
1774
|
+
resultPayload.workspace_memory = workspaceMemory;
|
|
1775
|
+
artifacts.push({
|
|
1776
|
+
id: `workspace_memory.${workspaceContext.workspaceId}`,
|
|
1777
|
+
kind: "workspace_memory",
|
|
1778
|
+
summary: workspaceMemory.summary,
|
|
1779
|
+
metadata: {
|
|
1780
|
+
workspace_id: workspaceContext.workspaceId,
|
|
1781
|
+
job_count: workspaceMemory.job_count,
|
|
1782
|
+
latest_job_id: workspaceMemory.latest_job_id || undefined,
|
|
1783
|
+
latest_job_status: workspaceMemory.latest_job_status || undefined,
|
|
1784
|
+
recent_action_count: workspaceMemory.recent_action_types.length,
|
|
1785
|
+
recent_target_count: workspaceMemory.recent_target_paths.length,
|
|
1786
|
+
recent_artifact_kind_count: workspaceMemory.recent_artifact_kinds.length,
|
|
1787
|
+
},
|
|
1788
|
+
});
|
|
1789
|
+
};
|
|
1790
|
+
let currentActionStepId;
|
|
1791
|
+
const appendHookEvent = (eventType, options) => {
|
|
1792
|
+
hookTrace.push({
|
|
1793
|
+
hook_id: `hook.${String(hookTrace.length + 1).padStart(3, "0")}`,
|
|
1794
|
+
event_type: eventType,
|
|
1795
|
+
recorded_at: new Date().toISOString(),
|
|
1796
|
+
source: "bridge_runtime",
|
|
1797
|
+
graph_id: runtimeManifest.graphId || undefined,
|
|
1798
|
+
step_id: options?.stepId || undefined,
|
|
1799
|
+
worker_id: (options?.stepId ? stepWorkerIdById.get(options.stepId) : undefined) || undefined,
|
|
1800
|
+
message: options?.message || undefined,
|
|
1801
|
+
metadata: options?.metadata || undefined,
|
|
1802
|
+
});
|
|
1803
|
+
};
|
|
1804
|
+
try {
|
|
1302
1805
|
for (let index = 0; index < actions.length; index += 1) {
|
|
1303
1806
|
this.assertNotCancelled(job.job_id);
|
|
1304
1807
|
const action = actions[index];
|
|
1305
1808
|
const progressPercent = Math.max(10, Math.round(((index + 1) / actions.length) * 100));
|
|
1809
|
+
currentActionStepId = runtimeStepIdForActionIndex(runtimeManifest, index);
|
|
1810
|
+
const stepReporter = bindStepReporter(reporter, currentActionStepId);
|
|
1811
|
+
if (workspaceContext) {
|
|
1812
|
+
assertActionAllowedByWorkspacePolicy(workspaceContext, action.type);
|
|
1813
|
+
}
|
|
1814
|
+
appendHookEvent("pre_tool_use", {
|
|
1815
|
+
stepId: currentActionStepId,
|
|
1816
|
+
message: `Preparando ${action.type}.`,
|
|
1817
|
+
metadata: {
|
|
1818
|
+
action_type: action.type,
|
|
1819
|
+
action_index: index,
|
|
1820
|
+
},
|
|
1821
|
+
});
|
|
1822
|
+
const reportActionProgress = async (progressMessage) => {
|
|
1823
|
+
await stepReporter.progress(progressPercent, progressMessage);
|
|
1824
|
+
};
|
|
1825
|
+
const appendActionArtifact = (kind, payload) => {
|
|
1826
|
+
appendInlineRuntimeArtifact(artifacts, runtimeManifest, index, kind, payload, currentActionStepId);
|
|
1827
|
+
};
|
|
1306
1828
|
if (action.type === "open_app") {
|
|
1307
|
-
await
|
|
1829
|
+
await reportActionProgress(`Abrindo ${action.app} no macOS`);
|
|
1308
1830
|
await this.openApp(action.app);
|
|
1831
|
+
resultPayload.last_app_action = {
|
|
1832
|
+
action: "open_app",
|
|
1833
|
+
app: action.app,
|
|
1834
|
+
focused: true,
|
|
1835
|
+
};
|
|
1836
|
+
appendActionArtifact("app_state", {
|
|
1837
|
+
summary: `${action.app} foi aberto no macOS.`,
|
|
1838
|
+
app: action.app,
|
|
1839
|
+
});
|
|
1309
1840
|
completionNotes.push(`${action.app} foi aberto no macOS.`);
|
|
1841
|
+
appendHookEvent("post_tool_use", {
|
|
1842
|
+
stepId: currentActionStepId,
|
|
1843
|
+
message: `${action.type} concluido.`,
|
|
1844
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
1845
|
+
});
|
|
1310
1846
|
continue;
|
|
1311
1847
|
}
|
|
1312
1848
|
if (action.type === "focus_app") {
|
|
1313
|
-
await
|
|
1849
|
+
await reportActionProgress(`Trazendo ${action.app} para frente`);
|
|
1314
1850
|
await this.focusApp(action.app);
|
|
1851
|
+
resultPayload.last_app_action = {
|
|
1852
|
+
action: "focus_app",
|
|
1853
|
+
app: action.app,
|
|
1854
|
+
focused: true,
|
|
1855
|
+
};
|
|
1856
|
+
appendActionArtifact("app_state", {
|
|
1857
|
+
summary: `${action.app} ficou em foco no macOS.`,
|
|
1858
|
+
app: action.app,
|
|
1859
|
+
});
|
|
1315
1860
|
completionNotes.push(`${action.app} ficou em foco no macOS.`);
|
|
1861
|
+
appendHookEvent("post_tool_use", {
|
|
1862
|
+
stepId: currentActionStepId,
|
|
1863
|
+
message: `${action.type} concluido.`,
|
|
1864
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
1865
|
+
});
|
|
1316
1866
|
continue;
|
|
1317
1867
|
}
|
|
1318
1868
|
if (action.type === "press_shortcut") {
|
|
1319
|
-
await
|
|
1869
|
+
await reportActionProgress(`Enviando atalho ${action.shortcut}`);
|
|
1320
1870
|
const shortcutResult = await this.pressShortcut(action.shortcut);
|
|
1871
|
+
resultPayload.last_shortcut = {
|
|
1872
|
+
shortcut: action.shortcut,
|
|
1873
|
+
performed: shortcutResult.performed,
|
|
1874
|
+
reason: shortcutResult.reason || null,
|
|
1875
|
+
};
|
|
1876
|
+
appendActionArtifact("app_state", {
|
|
1877
|
+
summary: shortcutResult.performed
|
|
1878
|
+
? `Atalho ${action.shortcut} enviado no macOS.`
|
|
1879
|
+
: (shortcutResult.reason || `O atalho ${action.shortcut} foi pulado.`),
|
|
1880
|
+
shortcut: action.shortcut,
|
|
1881
|
+
performed: shortcutResult.performed,
|
|
1882
|
+
});
|
|
1321
1883
|
if (action.shortcut.startsWith("media_")) {
|
|
1322
1884
|
const mediaSummaryMap = {
|
|
1323
1885
|
media_next: "Acionei o comando de próxima mídia no macOS.",
|
|
@@ -1339,14 +1901,23 @@ export class NativeMacOSJobExecutor {
|
|
|
1339
1901
|
continue;
|
|
1340
1902
|
}
|
|
1341
1903
|
if (action.type === "create_note") {
|
|
1342
|
-
await
|
|
1904
|
+
await reportActionProgress("Criando nota no Notes");
|
|
1343
1905
|
const noteTitle = await this.createNote(action.text, action.title);
|
|
1906
|
+
resultPayload.note = {
|
|
1907
|
+
title: noteTitle,
|
|
1908
|
+
text_preview: clipText(action.text, 180),
|
|
1909
|
+
};
|
|
1910
|
+
appendActionArtifact("local_note", {
|
|
1911
|
+
summary: `Nota criada no Notes: ${noteTitle}`,
|
|
1912
|
+
title: noteTitle,
|
|
1913
|
+
mime_type: "text/plain",
|
|
1914
|
+
});
|
|
1344
1915
|
completionNotes.push(`Nota criada no Notes: ${noteTitle}`);
|
|
1345
1916
|
continue;
|
|
1346
1917
|
}
|
|
1347
1918
|
if (action.type === "type_text") {
|
|
1348
1919
|
const typingApp = this.lastActiveApp || await this.getFrontmostAppName();
|
|
1349
|
-
await
|
|
1920
|
+
await reportActionProgress(`Digitando texto em ${typingApp || "app ativo"}`);
|
|
1350
1921
|
const typed = await this.guidedTypeText(action.text, typingApp || undefined);
|
|
1351
1922
|
if (!typed.ok) {
|
|
1352
1923
|
throw new Error(typed.reason || "Nao consegui digitar o texto no app ativo.");
|
|
@@ -1358,6 +1929,15 @@ export class NativeMacOSJobExecutor {
|
|
|
1358
1929
|
attempts: typed.attempts,
|
|
1359
1930
|
text_preview: clipText(action.text, 180),
|
|
1360
1931
|
};
|
|
1932
|
+
appendActionArtifact("typed_text", {
|
|
1933
|
+
summary: typed.verified
|
|
1934
|
+
? `Digitei e confirmei o texto no ${typed.app || "app ativo"}.`
|
|
1935
|
+
: `Digitei o texto no ${typed.app || "app ativo"}.`,
|
|
1936
|
+
mime_type: "text/plain",
|
|
1937
|
+
app: typed.app || null,
|
|
1938
|
+
verified: typed.verified,
|
|
1939
|
+
text_preview: clipText(action.text, 180),
|
|
1940
|
+
});
|
|
1361
1941
|
this.lastVisualTargetDescription = null;
|
|
1362
1942
|
this.lastVisualTargetApp = null;
|
|
1363
1943
|
completionNotes.push(typed.verified
|
|
@@ -1366,14 +1946,16 @@ export class NativeMacOSJobExecutor {
|
|
|
1366
1946
|
continue;
|
|
1367
1947
|
}
|
|
1368
1948
|
if (action.type === "take_screenshot") {
|
|
1369
|
-
await
|
|
1949
|
+
await reportActionProgress("Capturando screenshot do Mac");
|
|
1370
1950
|
const screenshotPath = await this.takeScreenshot(action.path);
|
|
1371
1951
|
const uploadable = await this.buildUploadableImage(screenshotPath);
|
|
1952
|
+
const expectedArtifactId = runtimeExpectedArtifactIdForActionIndex(runtimeManifest, index, "screenshot");
|
|
1372
1953
|
const screenshotArtifact = await this.uploadArtifactForJob(job.job_id, uploadable.path, {
|
|
1373
1954
|
kind: "screenshot",
|
|
1374
1955
|
mimeTypeOverride: uploadable.mimeType,
|
|
1375
1956
|
fileNameOverride: uploadable.filename,
|
|
1376
1957
|
metadata: {
|
|
1958
|
+
expected_artifact_id: expectedArtifactId || undefined,
|
|
1377
1959
|
visible_in_chat: true,
|
|
1378
1960
|
width: uploadable.dimensions?.width || undefined,
|
|
1379
1961
|
height: uploadable.dimensions?.height || undefined,
|
|
@@ -1393,14 +1975,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1393
1975
|
continue;
|
|
1394
1976
|
}
|
|
1395
1977
|
if (action.type === "read_frontmost_page") {
|
|
1396
|
-
await
|
|
1978
|
+
await reportActionProgress(`Lendo a pagina ativa em ${action.app || "Safari"}`);
|
|
1397
1979
|
const page = await this.readFrontmostPage(action.app || "Safari");
|
|
1398
1980
|
this.lastReadFrontmostPage = {
|
|
1399
1981
|
app: action.app || "Safari",
|
|
1400
1982
|
...page,
|
|
1401
1983
|
};
|
|
1402
1984
|
if (!page.text && this.bridgeConfig?.apiBaseUrl && this.bridgeConfig?.deviceToken) {
|
|
1403
|
-
await
|
|
1985
|
+
await reportActionProgress("Safari bloqueou leitura direta; vou analisar a pagina pela tela");
|
|
1404
1986
|
const screenshotPath = await this.takeScreenshot();
|
|
1405
1987
|
const uploadable = await this.buildUploadableImage(screenshotPath);
|
|
1406
1988
|
const artifact = await this.uploadArtifactForJob(job.job_id, uploadable.path, {
|
|
@@ -1429,73 +2011,424 @@ export class NativeMacOSJobExecutor {
|
|
|
1429
2011
|
}
|
|
1430
2012
|
}
|
|
1431
2013
|
resultPayload.page = page;
|
|
2014
|
+
appendActionArtifact("page_snapshot", {
|
|
2015
|
+
summary: `Li a pagina ${page.title || page.url || "ativa"} no navegador.`,
|
|
2016
|
+
mime_type: "text/plain",
|
|
2017
|
+
title: page.title || undefined,
|
|
2018
|
+
path: page.url || undefined,
|
|
2019
|
+
});
|
|
1432
2020
|
completionNotes.push(`Li a pagina ${page.title || page.url || "ativa"} no navegador.`);
|
|
1433
2021
|
continue;
|
|
1434
2022
|
}
|
|
1435
2023
|
if (action.type === "browser_context") {
|
|
1436
|
-
await
|
|
2024
|
+
await reportActionProgress("Lendo o contexto do navegador ativo");
|
|
1437
2025
|
const browserContext = await this.collectBrowserContext(action.app, action.include_text === true, action.include_tabs === true);
|
|
1438
2026
|
resultPayload.browser_context = browserContext;
|
|
1439
2027
|
resultPayload.summary = browserContext.summary;
|
|
2028
|
+
appendActionArtifact("browser_context", {
|
|
2029
|
+
summary: browserContext.summary,
|
|
2030
|
+
title: browserContext.title || undefined,
|
|
2031
|
+
path: browserContext.url || undefined,
|
|
2032
|
+
});
|
|
1440
2033
|
completionNotes.push(browserContext.summary);
|
|
1441
2034
|
continue;
|
|
1442
2035
|
}
|
|
1443
2036
|
if (action.type === "app_status") {
|
|
1444
|
-
await
|
|
2037
|
+
await reportActionProgress("Lendo os apps ativos do Mac");
|
|
1445
2038
|
const appStatus = await this.collectAppStatus(action.app, action.include_frontmost === true, action.include_running_apps === true, action.include_top_processes === true);
|
|
1446
2039
|
resultPayload.app_status = appStatus;
|
|
1447
2040
|
resultPayload.summary = appStatus.summary;
|
|
2041
|
+
appendActionArtifact("app_status", {
|
|
2042
|
+
summary: appStatus.summary,
|
|
2043
|
+
app: action.app || appStatus.frontmost_app || undefined,
|
|
2044
|
+
});
|
|
1448
2045
|
completionNotes.push(appStatus.summary);
|
|
1449
2046
|
continue;
|
|
1450
2047
|
}
|
|
1451
2048
|
if (action.type === "filesystem_inspect") {
|
|
1452
|
-
await
|
|
1453
|
-
const filesystem = await this.inspectFilesystemPath(action.path, action.include_children === true, action.include_preview === true, action.limit);
|
|
2049
|
+
await reportActionProgress(`Inspecionando ${action.path}`);
|
|
2050
|
+
const filesystem = await this.inspectFilesystemPath(action.path, action.include_children === true, action.include_preview === true, action.limit, workspaceContext);
|
|
1454
2051
|
resultPayload.filesystem = filesystem;
|
|
1455
2052
|
resultPayload.summary = filesystem.summary;
|
|
2053
|
+
appendActionArtifact("filesystem_snapshot", {
|
|
2054
|
+
summary: filesystem.summary,
|
|
2055
|
+
path: filesystem.resolved_path,
|
|
2056
|
+
});
|
|
1456
2057
|
completionNotes.push(filesystem.summary);
|
|
2058
|
+
appendHookEvent("post_tool_use", {
|
|
2059
|
+
stepId: currentActionStepId,
|
|
2060
|
+
message: `${action.type} concluido.`,
|
|
2061
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2062
|
+
});
|
|
1457
2063
|
continue;
|
|
1458
2064
|
}
|
|
1459
2065
|
if (action.type === "read_file") {
|
|
1460
|
-
await
|
|
1461
|
-
const fileContent = await this.readLocalFileSnapshot(action.path, action.max_chars);
|
|
2066
|
+
await reportActionProgress(`Lendo ${action.path}`);
|
|
2067
|
+
const fileContent = await this.readLocalFileSnapshot(action.path, action.max_chars, workspaceContext);
|
|
1462
2068
|
resultPayload.read_file = fileContent;
|
|
1463
2069
|
resultPayload.summary = fileContent.summary;
|
|
2070
|
+
appendActionArtifact("file_content", {
|
|
2071
|
+
summary: fileContent.summary,
|
|
2072
|
+
path: fileContent.resolved_path,
|
|
2073
|
+
mime_type: fileContent.mime_type,
|
|
2074
|
+
filename: fileContent.name,
|
|
2075
|
+
});
|
|
1464
2076
|
completionNotes.push(fileContent.summary);
|
|
2077
|
+
appendHookEvent("post_tool_use", {
|
|
2078
|
+
stepId: currentActionStepId,
|
|
2079
|
+
message: `${action.type} concluido.`,
|
|
2080
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2081
|
+
});
|
|
1465
2082
|
continue;
|
|
1466
2083
|
}
|
|
1467
2084
|
if (action.type === "trash_path") {
|
|
1468
|
-
await
|
|
1469
|
-
const trashed = await this.movePathToTrashSnapshot(action.path);
|
|
2085
|
+
await reportActionProgress(`Movendo ${action.path} para a Lixeira`);
|
|
2086
|
+
const trashed = await this.movePathToTrashSnapshot(action.path, workspaceContext);
|
|
1470
2087
|
resultPayload.trash_path = trashed;
|
|
1471
2088
|
resultPayload.summary = trashed.summary;
|
|
2089
|
+
appendActionArtifact("trash_receipt", {
|
|
2090
|
+
summary: trashed.summary,
|
|
2091
|
+
path: trashed.trashed_path,
|
|
2092
|
+
filename: trashed.name,
|
|
2093
|
+
});
|
|
1472
2094
|
completionNotes.push(trashed.summary);
|
|
2095
|
+
appendHookEvent("post_tool_use", {
|
|
2096
|
+
stepId: currentActionStepId,
|
|
2097
|
+
message: `${action.type} concluido.`,
|
|
2098
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2099
|
+
});
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
if (action.type === "delete_file") {
|
|
2103
|
+
await reportActionProgress(`Apagando arquivo local ${action.path}`);
|
|
2104
|
+
const deleted = await this.deleteLocalFileSnapshot(action.path, workspaceContext);
|
|
2105
|
+
resultPayload.delete_file = deleted;
|
|
2106
|
+
resultPayload.summary = deleted.summary;
|
|
2107
|
+
appendActionArtifact("delete_receipt", {
|
|
2108
|
+
summary: deleted.summary,
|
|
2109
|
+
path: deleted.resolved_path,
|
|
2110
|
+
filename: deleted.name,
|
|
2111
|
+
});
|
|
2112
|
+
completionNotes.push(deleted.summary);
|
|
2113
|
+
appendHookEvent("post_tool_use", {
|
|
2114
|
+
stepId: currentActionStepId,
|
|
2115
|
+
message: `${action.type} concluido.`,
|
|
2116
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2117
|
+
});
|
|
1473
2118
|
continue;
|
|
1474
2119
|
}
|
|
1475
2120
|
if (action.type === "write_text_file") {
|
|
1476
2121
|
const targetLabel = action.filename ? `${action.path}/${action.filename}` : action.path;
|
|
1477
|
-
await
|
|
2122
|
+
await reportActionProgress(`Escrevendo arquivo de texto em ${targetLabel}`);
|
|
1478
2123
|
const resolvedContent = this.resolveWriteTextFileContent(action);
|
|
1479
2124
|
if (!resolvedContent) {
|
|
1480
2125
|
throw new Error("Nenhum texto foi informado para gravar no arquivo local.");
|
|
1481
2126
|
}
|
|
1482
|
-
const written = await this.writeTextFileSnapshot(action.path, resolvedContent.content, action.filename, action.append === true, resolvedContent.source);
|
|
2127
|
+
const written = await this.writeTextFileSnapshot(action.path, resolvedContent.content, action.filename, action.append === true, resolvedContent.source, workspaceContext);
|
|
1483
2128
|
resultPayload.write_text_file = written;
|
|
1484
2129
|
resultPayload.summary = written.summary;
|
|
2130
|
+
appendActionArtifact("local_file", {
|
|
2131
|
+
summary: written.summary,
|
|
2132
|
+
path: written.resolved_path,
|
|
2133
|
+
filename: written.name,
|
|
2134
|
+
mime_type: written.mime_type,
|
|
2135
|
+
});
|
|
2136
|
+
completionNotes.push(written.summary);
|
|
2137
|
+
appendHookEvent("post_tool_use", {
|
|
2138
|
+
stepId: currentActionStepId,
|
|
2139
|
+
message: `${action.type} concluido.`,
|
|
2140
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2141
|
+
});
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
if (action.type === "write_json_file") {
|
|
2145
|
+
const targetLabel = action.filename ? `${action.path}/${action.filename}` : action.path;
|
|
2146
|
+
await reportActionProgress(`Escrevendo arquivo JSON em ${targetLabel}`);
|
|
2147
|
+
const written = await this.writeJsonFileSnapshot(action.path, action.data, action.filename, action.pretty !== false, workspaceContext);
|
|
2148
|
+
resultPayload.write_json_file = written;
|
|
2149
|
+
resultPayload.summary = written.summary;
|
|
2150
|
+
appendActionArtifact("local_file", {
|
|
2151
|
+
summary: written.summary,
|
|
2152
|
+
path: written.resolved_path,
|
|
2153
|
+
filename: written.name,
|
|
2154
|
+
mime_type: written.mime_type,
|
|
2155
|
+
});
|
|
1485
2156
|
completionNotes.push(written.summary);
|
|
2157
|
+
appendHookEvent("post_tool_use", {
|
|
2158
|
+
stepId: currentActionStepId,
|
|
2159
|
+
message: `${action.type} concluido.`,
|
|
2160
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2161
|
+
});
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
if (action.type === "apply_patch") {
|
|
2165
|
+
await reportActionProgress(`Aplicando patch local em ${action.cwd}`);
|
|
2166
|
+
const patchSet = await this.applyPatchSnapshot(action.patch, action.cwd, workspaceContext);
|
|
2167
|
+
resultPayload.patch_set = patchSet;
|
|
2168
|
+
resultPayload.summary = patchSet.summary;
|
|
2169
|
+
appendActionArtifact("patch_set", {
|
|
2170
|
+
summary: patchSet.summary,
|
|
2171
|
+
path: patchSet.resolved_cwd,
|
|
2172
|
+
mime_type: "application/json",
|
|
2173
|
+
});
|
|
2174
|
+
completionNotes.push(patchSet.summary);
|
|
2175
|
+
appendHookEvent("post_tool_use", {
|
|
2176
|
+
stepId: currentActionStepId,
|
|
2177
|
+
message: `${action.type} concluido.`,
|
|
2178
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2179
|
+
});
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
if (action.type === "mkdir") {
|
|
2183
|
+
await reportActionProgress(`Criando pasta em ${action.path}`);
|
|
2184
|
+
const created = await this.createDirectorySnapshot(action.path, action.create_parents !== false, workspaceContext);
|
|
2185
|
+
resultPayload.mkdir = created;
|
|
2186
|
+
resultPayload.summary = created.summary;
|
|
2187
|
+
appendActionArtifact("directory_receipt", {
|
|
2188
|
+
summary: created.summary,
|
|
2189
|
+
path: created.resolved_path,
|
|
2190
|
+
filename: created.name,
|
|
2191
|
+
});
|
|
2192
|
+
completionNotes.push(created.summary);
|
|
2193
|
+
appendHookEvent("post_tool_use", {
|
|
2194
|
+
stepId: currentActionStepId,
|
|
2195
|
+
message: `${action.type} concluido.`,
|
|
2196
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2197
|
+
});
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
if (action.type === "move_file") {
|
|
2201
|
+
await reportActionProgress(`Movendo item local de ${action.source_path} para ${action.destination_path}`);
|
|
2202
|
+
const moved = await this.moveLocalPathSnapshot(action.source_path, action.destination_path, action.overwrite === true, workspaceContext);
|
|
2203
|
+
resultPayload.move_file = moved;
|
|
2204
|
+
resultPayload.summary = moved.summary;
|
|
2205
|
+
appendActionArtifact("move_receipt", {
|
|
2206
|
+
summary: moved.summary,
|
|
2207
|
+
path: moved.resolved_destination_path,
|
|
2208
|
+
filename: moved.name,
|
|
2209
|
+
});
|
|
2210
|
+
completionNotes.push(moved.summary);
|
|
2211
|
+
appendHookEvent("post_tool_use", {
|
|
2212
|
+
stepId: currentActionStepId,
|
|
2213
|
+
message: `${action.type} concluido.`,
|
|
2214
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2215
|
+
});
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
if (action.type === "git_clone") {
|
|
2219
|
+
await reportActionProgress(`Clonando repositório em ${action.destination_path}`);
|
|
2220
|
+
const gitClone = await this.gitCloneSnapshot(action.repository, action.destination_path, {
|
|
2221
|
+
branch: action.branch,
|
|
2222
|
+
depth: action.depth,
|
|
2223
|
+
}, workspaceContext);
|
|
2224
|
+
resultPayload.git_clone = gitClone;
|
|
2225
|
+
resultPayload.summary = gitClone.summary;
|
|
2226
|
+
appendActionArtifact("git_clone_receipt", {
|
|
2227
|
+
summary: gitClone.summary,
|
|
2228
|
+
path: gitClone.repo_root || gitClone.resolved_destination_path,
|
|
2229
|
+
mime_type: "application/json",
|
|
2230
|
+
});
|
|
2231
|
+
completionNotes.push(gitClone.summary);
|
|
2232
|
+
appendHookEvent("post_tool_use", {
|
|
2233
|
+
stepId: currentActionStepId,
|
|
2234
|
+
message: `${action.type} concluido.`,
|
|
2235
|
+
metadata: { action_type: action.type, status: gitClone.cloned ? "completed" : "failed" },
|
|
2236
|
+
});
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
if (action.type === "git_fetch") {
|
|
2240
|
+
await reportActionProgress(`Atualizando refs remotos do Git em ${action.cwd}`);
|
|
2241
|
+
const gitFetch = await this.gitFetchSnapshot(action.cwd, {
|
|
2242
|
+
remote: action.remote,
|
|
2243
|
+
prune: action.prune === true,
|
|
2244
|
+
tags: action.tags === true,
|
|
2245
|
+
}, workspaceContext);
|
|
2246
|
+
resultPayload.git_fetch = gitFetch;
|
|
2247
|
+
resultPayload.summary = gitFetch.summary;
|
|
2248
|
+
appendActionArtifact("git_fetch_receipt", {
|
|
2249
|
+
summary: gitFetch.summary,
|
|
2250
|
+
path: gitFetch.repo_root || gitFetch.resolved_cwd,
|
|
2251
|
+
mime_type: "application/json",
|
|
2252
|
+
});
|
|
2253
|
+
completionNotes.push(gitFetch.summary);
|
|
2254
|
+
appendHookEvent("post_tool_use", {
|
|
2255
|
+
stepId: currentActionStepId,
|
|
2256
|
+
message: `${action.type} concluido.`,
|
|
2257
|
+
metadata: { action_type: action.type, status: gitFetch.fetched === false ? "failed" : "completed" },
|
|
2258
|
+
});
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
if (action.type === "git_checkout") {
|
|
2262
|
+
await reportActionProgress(`Trocando branch local em ${action.cwd}`);
|
|
2263
|
+
const gitCheckout = await this.gitCheckoutSnapshot(action.cwd, {
|
|
2264
|
+
target: action.target,
|
|
2265
|
+
startPoint: action.start_point,
|
|
2266
|
+
createBranch: action.create_branch === true,
|
|
2267
|
+
detach: action.detach === true,
|
|
2268
|
+
}, workspaceContext);
|
|
2269
|
+
resultPayload.git_checkout = gitCheckout;
|
|
2270
|
+
resultPayload.summary = gitCheckout.summary;
|
|
2271
|
+
appendActionArtifact("git_checkout_receipt", {
|
|
2272
|
+
summary: gitCheckout.summary,
|
|
2273
|
+
path: gitCheckout.repo_root || gitCheckout.resolved_cwd,
|
|
2274
|
+
mime_type: "application/json",
|
|
2275
|
+
});
|
|
2276
|
+
completionNotes.push(gitCheckout.summary);
|
|
2277
|
+
appendHookEvent("post_tool_use", {
|
|
2278
|
+
stepId: currentActionStepId,
|
|
2279
|
+
message: `${action.type} concluido.`,
|
|
2280
|
+
metadata: { action_type: action.type, status: gitCheckout.switched === false ? "failed" : "completed" },
|
|
2281
|
+
});
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
if (action.type === "git_rebase") {
|
|
2285
|
+
await reportActionProgress(`Rebaseando a branch atual em ${action.cwd}`);
|
|
2286
|
+
const gitRebase = await this.gitRebaseSnapshot(action.cwd, {
|
|
2287
|
+
target: action.target,
|
|
2288
|
+
autostash: action.autostash === true,
|
|
2289
|
+
}, workspaceContext);
|
|
2290
|
+
resultPayload.git_rebase = gitRebase;
|
|
2291
|
+
resultPayload.summary = gitRebase.summary;
|
|
2292
|
+
appendActionArtifact("git_rebase_receipt", {
|
|
2293
|
+
summary: gitRebase.summary,
|
|
2294
|
+
path: gitRebase.repo_root || gitRebase.resolved_cwd,
|
|
2295
|
+
mime_type: "application/json",
|
|
2296
|
+
});
|
|
2297
|
+
completionNotes.push(gitRebase.summary);
|
|
2298
|
+
appendHookEvent("post_tool_use", {
|
|
2299
|
+
stepId: currentActionStepId,
|
|
2300
|
+
message: `${action.type} concluido.`,
|
|
2301
|
+
metadata: { action_type: action.type, status: gitRebase.rebased === false ? "failed" : "completed" },
|
|
2302
|
+
});
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
if (action.type === "git_merge") {
|
|
2306
|
+
await reportActionProgress(`Mesclando ${action.target} em ${action.cwd}`);
|
|
2307
|
+
const gitMerge = await this.gitMergeSnapshot(action.cwd, {
|
|
2308
|
+
target: action.target,
|
|
2309
|
+
ffOnly: action.ff_only === true,
|
|
2310
|
+
noFf: action.no_ff === true,
|
|
2311
|
+
message: action.message,
|
|
2312
|
+
}, workspaceContext);
|
|
2313
|
+
resultPayload.git_merge = gitMerge;
|
|
2314
|
+
resultPayload.summary = gitMerge.summary;
|
|
2315
|
+
appendActionArtifact("git_merge_receipt", {
|
|
2316
|
+
summary: gitMerge.summary,
|
|
2317
|
+
path: gitMerge.repo_root || gitMerge.resolved_cwd,
|
|
2318
|
+
mime_type: "application/json",
|
|
2319
|
+
});
|
|
2320
|
+
completionNotes.push(gitMerge.summary);
|
|
2321
|
+
appendHookEvent("post_tool_use", {
|
|
2322
|
+
stepId: currentActionStepId,
|
|
2323
|
+
message: `${action.type} concluido.`,
|
|
2324
|
+
metadata: { action_type: action.type, status: gitMerge.merged === false ? "failed" : "completed" },
|
|
2325
|
+
});
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
if (action.type === "git_tag") {
|
|
2329
|
+
await reportActionProgress(`Criando a tag ${action.name} em ${action.cwd}`);
|
|
2330
|
+
const gitTag = await this.gitTagSnapshot(action.cwd, {
|
|
2331
|
+
name: action.name,
|
|
2332
|
+
target: action.target,
|
|
2333
|
+
annotated: action.annotated === true,
|
|
2334
|
+
message: action.message,
|
|
2335
|
+
}, workspaceContext);
|
|
2336
|
+
resultPayload.git_tag = gitTag;
|
|
2337
|
+
resultPayload.summary = gitTag.summary;
|
|
2338
|
+
appendActionArtifact("git_tag_receipt", {
|
|
2339
|
+
summary: gitTag.summary,
|
|
2340
|
+
path: gitTag.repo_root || gitTag.resolved_cwd,
|
|
2341
|
+
mime_type: "application/json",
|
|
2342
|
+
});
|
|
2343
|
+
completionNotes.push(gitTag.summary);
|
|
2344
|
+
appendHookEvent("post_tool_use", {
|
|
2345
|
+
stepId: currentActionStepId,
|
|
2346
|
+
message: `${action.type} concluido.`,
|
|
2347
|
+
metadata: { action_type: action.type, status: gitTag.created === false ? "failed" : "completed" },
|
|
2348
|
+
});
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
if (action.type === "git_add") {
|
|
2352
|
+
await reportActionProgress(`Preparando stage do Git em ${action.cwd}`);
|
|
2353
|
+
const gitAdd = await this.gitAddSnapshot(action.cwd, {
|
|
2354
|
+
paths: action.paths,
|
|
2355
|
+
all: action.all === true,
|
|
2356
|
+
}, workspaceContext);
|
|
2357
|
+
resultPayload.git_add = gitAdd;
|
|
2358
|
+
resultPayload.summary = gitAdd.summary;
|
|
2359
|
+
appendActionArtifact("git_stage_receipt", {
|
|
2360
|
+
summary: gitAdd.summary,
|
|
2361
|
+
path: gitAdd.repo_root || gitAdd.resolved_cwd,
|
|
2362
|
+
mime_type: "application/json",
|
|
2363
|
+
});
|
|
2364
|
+
completionNotes.push(gitAdd.summary);
|
|
2365
|
+
appendHookEvent("post_tool_use", {
|
|
2366
|
+
stepId: currentActionStepId,
|
|
2367
|
+
message: `${action.type} concluido.`,
|
|
2368
|
+
metadata: { action_type: action.type, status: gitAdd.error_message ? "failed" : "completed" },
|
|
2369
|
+
});
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (action.type === "git_commit") {
|
|
2373
|
+
await reportActionProgress(`Criando commit local em ${action.cwd}`);
|
|
2374
|
+
const gitCommit = await this.gitCommitSnapshot(action.cwd, action.message, action.allow_empty === true, workspaceContext);
|
|
2375
|
+
resultPayload.git_commit = gitCommit;
|
|
2376
|
+
resultPayload.summary = gitCommit.summary;
|
|
2377
|
+
appendActionArtifact("git_commit_receipt", {
|
|
2378
|
+
summary: gitCommit.summary,
|
|
2379
|
+
path: gitCommit.repo_root || gitCommit.resolved_cwd,
|
|
2380
|
+
mime_type: "application/json",
|
|
2381
|
+
});
|
|
2382
|
+
completionNotes.push(gitCommit.summary);
|
|
2383
|
+
appendHookEvent("post_tool_use", {
|
|
2384
|
+
stepId: currentActionStepId,
|
|
2385
|
+
message: `${action.type} concluido.`,
|
|
2386
|
+
metadata: { action_type: action.type, status: gitCommit.committed === false ? "failed" : "completed" },
|
|
2387
|
+
});
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
if (action.type === "git_push") {
|
|
2391
|
+
await reportActionProgress(`Enviando branch local para o remote em ${action.cwd}`);
|
|
2392
|
+
const gitPush = await this.gitPushSnapshot(action.cwd, {
|
|
2393
|
+
remote: action.remote,
|
|
2394
|
+
branch: action.branch,
|
|
2395
|
+
setUpstream: action.set_upstream === true,
|
|
2396
|
+
}, workspaceContext);
|
|
2397
|
+
resultPayload.git_push = gitPush;
|
|
2398
|
+
resultPayload.summary = gitPush.summary;
|
|
2399
|
+
appendActionArtifact("git_push_receipt", {
|
|
2400
|
+
summary: gitPush.summary,
|
|
2401
|
+
path: gitPush.repo_root || gitPush.resolved_cwd,
|
|
2402
|
+
mime_type: "application/json",
|
|
2403
|
+
});
|
|
2404
|
+
completionNotes.push(gitPush.summary);
|
|
2405
|
+
appendHookEvent("post_tool_use", {
|
|
2406
|
+
stepId: currentActionStepId,
|
|
2407
|
+
message: `${action.type} concluido.`,
|
|
2408
|
+
metadata: { action_type: action.type, status: gitPush.pushed === false ? "failed" : "completed" },
|
|
2409
|
+
});
|
|
1486
2410
|
continue;
|
|
1487
2411
|
}
|
|
1488
2412
|
if (action.type === "list_files") {
|
|
1489
|
-
await
|
|
1490
|
-
const listing = await this.listLocalFilesSnapshot(action.path, action.limit);
|
|
2413
|
+
await reportActionProgress(`Listando arquivos em ${action.path}`);
|
|
2414
|
+
const listing = await this.listLocalFilesSnapshot(action.path, action.limit, workspaceContext);
|
|
1491
2415
|
resultPayload.file_listing = listing;
|
|
1492
2416
|
resultPayload.summary = listing.summary;
|
|
2417
|
+
appendActionArtifact("file_listing", {
|
|
2418
|
+
summary: listing.summary,
|
|
2419
|
+
path: listing.resolved_path,
|
|
2420
|
+
});
|
|
1493
2421
|
completionNotes.push(listing.summary);
|
|
2422
|
+
appendHookEvent("post_tool_use", {
|
|
2423
|
+
stepId: currentActionStepId,
|
|
2424
|
+
message: `${action.type} concluido.`,
|
|
2425
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2426
|
+
});
|
|
1494
2427
|
continue;
|
|
1495
2428
|
}
|
|
1496
2429
|
if (action.type === "count_files") {
|
|
1497
|
-
await
|
|
1498
|
-
const counted = await this.countLocalFiles(action.path, action.extensions, action.recursive !== false);
|
|
2430
|
+
await reportActionProgress(`Contando arquivos em ${action.path}`);
|
|
2431
|
+
const counted = await this.countLocalFiles(action.path, action.extensions, action.recursive !== false, workspaceContext);
|
|
1499
2432
|
completionNotes.push(`Encontrei ${counted.total} arquivo${counted.total === 1 ? "" : "s"} ${counted.extensionsLabel} em ${counted.path}.`);
|
|
1500
2433
|
resultPayload.file_count = {
|
|
1501
2434
|
total: counted.total,
|
|
@@ -1503,48 +2436,161 @@ export class NativeMacOSJobExecutor {
|
|
|
1503
2436
|
extensions: counted.extensions,
|
|
1504
2437
|
recursive: counted.recursive,
|
|
1505
2438
|
};
|
|
2439
|
+
appendActionArtifact("file_count", {
|
|
2440
|
+
summary: `Encontrei ${counted.total} arquivo${counted.total === 1 ? "" : "s"} ${counted.extensionsLabel} em ${counted.path}.`,
|
|
2441
|
+
path: counted.path,
|
|
2442
|
+
});
|
|
2443
|
+
appendHookEvent("post_tool_use", {
|
|
2444
|
+
stepId: currentActionStepId,
|
|
2445
|
+
message: `${action.type} concluido.`,
|
|
2446
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2447
|
+
});
|
|
1506
2448
|
continue;
|
|
1507
2449
|
}
|
|
1508
2450
|
if (action.type === "system_status") {
|
|
1509
|
-
await
|
|
2451
|
+
await reportActionProgress("Lendo CPU, memoria, disco e bateria do Mac");
|
|
1510
2452
|
const systemStatus = await this.collectSystemStatus(action.sections, action.include_top_processes === true);
|
|
1511
2453
|
resultPayload.system_status = systemStatus;
|
|
1512
2454
|
resultPayload.summary = systemStatus.summary;
|
|
2455
|
+
appendActionArtifact("system_status", {
|
|
2456
|
+
summary: systemStatus.summary,
|
|
2457
|
+
});
|
|
1513
2458
|
completionNotes.push(systemStatus.summary);
|
|
1514
2459
|
continue;
|
|
1515
2460
|
}
|
|
1516
|
-
if (action.type === "
|
|
1517
|
-
await
|
|
1518
|
-
const
|
|
1519
|
-
|
|
2461
|
+
if (action.type === "git_status") {
|
|
2462
|
+
await reportActionProgress(`Lendo o estado do Git em ${action.cwd}`);
|
|
2463
|
+
const gitStatus = await this.gitStatusSnapshot(action.cwd, action.include_untracked !== false, workspaceContext);
|
|
2464
|
+
resultPayload.git_status = gitStatus;
|
|
2465
|
+
resultPayload.summary = gitStatus.summary;
|
|
2466
|
+
appendActionArtifact("git_status_report", {
|
|
2467
|
+
summary: gitStatus.summary,
|
|
2468
|
+
path: gitStatus.repo_root || gitStatus.resolved_cwd,
|
|
2469
|
+
mime_type: "application/json",
|
|
2470
|
+
});
|
|
2471
|
+
completionNotes.push(gitStatus.summary);
|
|
2472
|
+
appendHookEvent("post_tool_use", {
|
|
2473
|
+
stepId: currentActionStepId,
|
|
2474
|
+
message: `${action.type} concluido.`,
|
|
2475
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2476
|
+
});
|
|
1520
2477
|
continue;
|
|
1521
2478
|
}
|
|
1522
|
-
if (action.type === "
|
|
1523
|
-
await
|
|
1524
|
-
await this.
|
|
1525
|
-
|
|
2479
|
+
if (action.type === "git_diff") {
|
|
2480
|
+
await reportActionProgress(`Lendo o diff do Git em ${action.cwd}`);
|
|
2481
|
+
const gitDiff = await this.gitDiffSnapshot(action.cwd, {
|
|
2482
|
+
staged: action.staged === true,
|
|
2483
|
+
baseRef: action.base_ref,
|
|
2484
|
+
paths: action.paths,
|
|
2485
|
+
maxChars: action.max_chars,
|
|
2486
|
+
}, workspaceContext);
|
|
2487
|
+
resultPayload.git_diff = gitDiff;
|
|
2488
|
+
resultPayload.summary = gitDiff.summary;
|
|
2489
|
+
appendActionArtifact("git_diff_report", {
|
|
2490
|
+
summary: gitDiff.summary,
|
|
2491
|
+
path: gitDiff.repo_root || gitDiff.resolved_cwd,
|
|
2492
|
+
mime_type: "text/plain",
|
|
2493
|
+
});
|
|
2494
|
+
completionNotes.push(gitDiff.summary);
|
|
2495
|
+
appendHookEvent("post_tool_use", {
|
|
2496
|
+
stepId: currentActionStepId,
|
|
2497
|
+
message: `${action.type} concluido.`,
|
|
2498
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2499
|
+
});
|
|
1526
2500
|
continue;
|
|
1527
2501
|
}
|
|
1528
|
-
if (action.type === "
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
2502
|
+
if (action.type === "run_tests") {
|
|
2503
|
+
await reportActionProgress(`Rodando testes em ${action.cwd}`);
|
|
2504
|
+
appendHookEvent("validation_ladder_started", {
|
|
2505
|
+
stepId: currentActionStepId,
|
|
2506
|
+
message: "Validation ladder iniciada.",
|
|
2507
|
+
metadata: {
|
|
2508
|
+
requested_profile: action.profile || undefined,
|
|
2509
|
+
ladder_id: runtimeManifest.validationLadder?.ladder_id || undefined,
|
|
2510
|
+
},
|
|
2511
|
+
});
|
|
2512
|
+
const testReport = await this.runTestsSnapshot(action.command, action.cwd, action.timeout_seconds, workspaceContext, action.profile, runtimeManifest);
|
|
2513
|
+
resultPayload.test_report = testReport;
|
|
2514
|
+
resultPayload.summary = testReport.summary;
|
|
2515
|
+
appendActionArtifact("test_report", {
|
|
2516
|
+
summary: testReport.summary,
|
|
2517
|
+
path: testReport.resolved_cwd,
|
|
2518
|
+
mime_type: "text/plain",
|
|
2519
|
+
});
|
|
2520
|
+
completionNotes.push(testReport.summary);
|
|
2521
|
+
appendHookEvent("validation_ladder_completed", {
|
|
2522
|
+
stepId: currentActionStepId,
|
|
2523
|
+
message: testReport.summary,
|
|
2524
|
+
metadata: {
|
|
2525
|
+
requested_profile: action.profile || undefined,
|
|
2526
|
+
stage_count: testReport.stage_count || 0,
|
|
2527
|
+
failed_stage_id: testReport.failed_stage_id || undefined,
|
|
2528
|
+
passed: testReport.passed,
|
|
2529
|
+
},
|
|
2530
|
+
});
|
|
2531
|
+
appendHookEvent("post_tool_use", {
|
|
2532
|
+
stepId: currentActionStepId,
|
|
2533
|
+
message: `${action.type} concluido.`,
|
|
2534
|
+
metadata: { action_type: action.type, status: testReport.passed ? "completed" : "failed" },
|
|
2535
|
+
});
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (action.type === "run_shell") {
|
|
2539
|
+
await reportActionProgress(`Rodando comando local: ${action.command}`);
|
|
2540
|
+
const shellOutput = await this.runShellCommand(action.command, action.cwd, workspaceContext);
|
|
2541
|
+
resultPayload.shell_result = {
|
|
2542
|
+
command: action.command,
|
|
2543
|
+
cwd: workspaceContext ? assertCwdInsideWorkspace(workspaceContext, action.cwd) : (action.cwd || null),
|
|
2544
|
+
output: shellOutput,
|
|
2545
|
+
};
|
|
2546
|
+
appendActionArtifact("shell_result", {
|
|
2547
|
+
summary: `Comando executado: ${action.command}`,
|
|
2548
|
+
mime_type: "text/plain",
|
|
2549
|
+
});
|
|
2550
|
+
completionNotes.push(`Saida de \`${action.command}\`:\n${shellOutput}`);
|
|
2551
|
+
appendHookEvent("post_tool_use", {
|
|
2552
|
+
stepId: currentActionStepId,
|
|
2553
|
+
message: `${action.type} concluido.`,
|
|
2554
|
+
metadata: { action_type: action.type, status: "completed" },
|
|
2555
|
+
});
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
if (action.type === "set_volume") {
|
|
2559
|
+
await reportActionProgress(`Ajustando volume para ${action.level}%`);
|
|
2560
|
+
await this.setVolume(action.level);
|
|
2561
|
+
resultPayload.device_state = {
|
|
2562
|
+
volume_level: action.level,
|
|
2563
|
+
};
|
|
2564
|
+
appendActionArtifact("device_state_change", {
|
|
2565
|
+
summary: `Volume ajustado para ${action.level}% no macOS.`,
|
|
2566
|
+
});
|
|
2567
|
+
completionNotes.push(`Volume ajustado para ${action.level}% no macOS.`);
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
if (action.type === "scroll_view") {
|
|
2571
|
+
const scrollApp = action.app || this.lastActiveApp || await this.getFrontmostAppName();
|
|
2572
|
+
if (scrollApp) {
|
|
2573
|
+
await reportActionProgress(`Trazendo ${scrollApp} para frente antes de rolar a tela`);
|
|
2574
|
+
await this.focusApp(scrollApp);
|
|
2575
|
+
}
|
|
2576
|
+
const directionLabel = action.direction === "up" ? "cima" : "baixo";
|
|
2577
|
+
await reportActionProgress(`Rolando a tela para ${directionLabel}`);
|
|
2578
|
+
await this.scrollView(action.direction, action.amount, action.steps);
|
|
2579
|
+
resultPayload.last_scroll = {
|
|
1538
2580
|
direction: action.direction,
|
|
1539
2581
|
amount: action.amount || "medium",
|
|
1540
2582
|
steps: action.steps || 1,
|
|
1541
2583
|
app: scrollApp || null,
|
|
1542
2584
|
};
|
|
2585
|
+
appendActionArtifact("viewport_change", {
|
|
2586
|
+
summary: `Rolei a tela para ${directionLabel} no macOS.`,
|
|
2587
|
+
app: scrollApp || null,
|
|
2588
|
+
});
|
|
1543
2589
|
completionNotes.push(`Rolei a tela para ${directionLabel} no macOS.`);
|
|
1544
2590
|
continue;
|
|
1545
2591
|
}
|
|
1546
2592
|
if (action.type === "whatsapp_send_message") {
|
|
1547
|
-
await
|
|
2593
|
+
await reportActionProgress(`Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1548
2594
|
await this.ensureWhatsAppWebReady();
|
|
1549
2595
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1550
2596
|
if (!selected) {
|
|
@@ -1554,7 +2600,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1554
2600
|
messages: [],
|
|
1555
2601
|
summary: "",
|
|
1556
2602
|
}));
|
|
1557
|
-
await
|
|
2603
|
+
await reportActionProgress(`Enviando a mensagem para ${action.contact} no WhatsApp`);
|
|
1558
2604
|
await this.sendWhatsAppMessage(action.text);
|
|
1559
2605
|
await delay(900);
|
|
1560
2606
|
const afterSend = await this.readWhatsAppVisibleConversation(action.contact, Math.max(12, beforeSend.messages.length + 4)).catch(() => null);
|
|
@@ -1565,17 +2611,22 @@ export class NativeMacOSJobExecutor {
|
|
|
1565
2611
|
messages: afterSend?.messages || [],
|
|
1566
2612
|
summary: afterSend?.summary || "",
|
|
1567
2613
|
};
|
|
2614
|
+
appendActionArtifact("message_delivery", {
|
|
2615
|
+
summary: `Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`,
|
|
2616
|
+
contact: action.contact,
|
|
2617
|
+
});
|
|
1568
2618
|
const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
|
|
1569
2619
|
if (!verification.ok) {
|
|
1570
2620
|
resultPayload.summary = verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`;
|
|
1571
|
-
|
|
2621
|
+
attachWorkspaceMemory("failed");
|
|
2622
|
+
await stepReporter.failed(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`, resultPayload);
|
|
1572
2623
|
return;
|
|
1573
2624
|
}
|
|
1574
2625
|
completionNotes.push(`Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`);
|
|
1575
2626
|
continue;
|
|
1576
2627
|
}
|
|
1577
2628
|
if (action.type === "whatsapp_read_chat") {
|
|
1578
|
-
await
|
|
2629
|
+
await reportActionProgress(`Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1579
2630
|
await this.ensureWhatsAppWebReady();
|
|
1580
2631
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1581
2632
|
if (!selected) {
|
|
@@ -1588,17 +2639,21 @@ export class NativeMacOSJobExecutor {
|
|
|
1588
2639
|
contact: action.contact,
|
|
1589
2640
|
messages: chat.messages,
|
|
1590
2641
|
};
|
|
2642
|
+
appendActionArtifact("message_snapshot", {
|
|
2643
|
+
summary: `Mensagens visiveis no WhatsApp com ${action.contact}.`,
|
|
2644
|
+
contact: action.contact,
|
|
2645
|
+
});
|
|
1591
2646
|
completionNotes.push(`Mensagens visiveis no WhatsApp com ${action.contact}:\n${chat.summary}`);
|
|
1592
2647
|
continue;
|
|
1593
2648
|
}
|
|
1594
2649
|
if (action.type === "click_visual_target") {
|
|
1595
2650
|
const browserApp = await this.resolveLikelyBrowserApp(action.app);
|
|
1596
2651
|
if (browserApp) {
|
|
1597
|
-
await
|
|
2652
|
+
await reportActionProgress(`Trazendo ${browserApp} para frente antes do clique`);
|
|
1598
2653
|
await this.focusApp(browserApp);
|
|
1599
2654
|
}
|
|
1600
2655
|
else if (action.app) {
|
|
1601
|
-
await
|
|
2656
|
+
await reportActionProgress(`Trazendo ${action.app} para frente antes do clique`);
|
|
1602
2657
|
await this.focusApp(action.app);
|
|
1603
2658
|
}
|
|
1604
2659
|
const targetDescriptions = isSpotifySafariDomOnlyStep(action.description)
|
|
@@ -1614,7 +2669,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1614
2669
|
const isSpotifySafariStep = browserApp === "Safari" && isSpotifySafariDomOnlyStep(targetDescription);
|
|
1615
2670
|
const verificationPrompt = isSpotifySafariStep ? undefined : action.verification_prompt;
|
|
1616
2671
|
if (isSpotifySafariStep) {
|
|
1617
|
-
await
|
|
2672
|
+
await reportActionProgress(`Tentando concluir ${targetDescription} pelo DOM do Spotify no Safari`);
|
|
1618
2673
|
const spotifyDomResult = await this.executeSpotifySafariDomStep(targetDescription, initialBrowserState);
|
|
1619
2674
|
if (spotifyDomResult.ok) {
|
|
1620
2675
|
this.rememberSatisfiedSpotifyStep(targetDescription, !!spotifyDomResult.confirmedPlaying);
|
|
@@ -1640,7 +2695,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1640
2695
|
}
|
|
1641
2696
|
const nativeMediaTransport = extractNativeMediaTransportCommand(targetDescription);
|
|
1642
2697
|
if (nativeMediaTransport) {
|
|
1643
|
-
await
|
|
2698
|
+
await reportActionProgress(`Tentando controle de mídia nativo do macOS para ${targetDescription}`);
|
|
1644
2699
|
try {
|
|
1645
2700
|
await this.triggerMacOSMediaTransport(nativeMediaTransport);
|
|
1646
2701
|
let validated = false;
|
|
@@ -1651,7 +2706,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1651
2706
|
validationReason = browserValidation.reason;
|
|
1652
2707
|
}
|
|
1653
2708
|
if (!validated && verificationPrompt) {
|
|
1654
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent,
|
|
2709
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, stepReporter, artifacts, "native_media_transport_result");
|
|
1655
2710
|
if (verification.unavailable) {
|
|
1656
2711
|
lastFailureReason = verification.reason;
|
|
1657
2712
|
break;
|
|
@@ -1672,14 +2727,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1672
2727
|
break;
|
|
1673
2728
|
}
|
|
1674
2729
|
lastFailureReason = validationReason || `O controle de mídia nativo do macOS nao confirmou ${targetDescription}.`;
|
|
1675
|
-
await
|
|
2730
|
+
await reportActionProgress("O controle de mídia nativo nao foi suficiente; vou tentar DOM/OCR");
|
|
1676
2731
|
}
|
|
1677
2732
|
catch (error) {
|
|
1678
2733
|
lastFailureReason = error instanceof Error ? error.message : String(error);
|
|
1679
2734
|
}
|
|
1680
2735
|
}
|
|
1681
2736
|
if (browserApp === "Safari") {
|
|
1682
|
-
await
|
|
2737
|
+
await reportActionProgress(`Tentando localizar ${targetDescription} diretamente no Safari`);
|
|
1683
2738
|
const domClick = await this.trySafariDomClick(targetDescription);
|
|
1684
2739
|
if (domClick?.clicked) {
|
|
1685
2740
|
let validated = false;
|
|
@@ -1690,7 +2745,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1690
2745
|
validationReason = browserValidation.reason;
|
|
1691
2746
|
}
|
|
1692
2747
|
if (!validated && verificationPrompt) {
|
|
1693
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent,
|
|
2748
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, stepReporter, artifacts, "dom_click_result");
|
|
1694
2749
|
if (verification.unavailable) {
|
|
1695
2750
|
lastFailureReason = verification.reason;
|
|
1696
2751
|
break;
|
|
@@ -1724,7 +2779,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1724
2779
|
const visualBeforeState = browserApp
|
|
1725
2780
|
? await this.captureBrowserPageState(browserApp).catch(() => initialBrowserState)
|
|
1726
2781
|
: initialBrowserState;
|
|
1727
|
-
await
|
|
2782
|
+
await reportActionProgress(`Capturando a tela para localizar ${targetDescription}`);
|
|
1728
2783
|
let screenshotPath = await this.takeScreenshot();
|
|
1729
2784
|
const ocrClick = await this.tryLocalOcrClick(screenshotPath, targetDescription);
|
|
1730
2785
|
if (ocrClick.clicked) {
|
|
@@ -1736,7 +2791,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1736
2791
|
validationReason = browserValidation.reason;
|
|
1737
2792
|
}
|
|
1738
2793
|
if (!validated && verificationPrompt) {
|
|
1739
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent,
|
|
2794
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, stepReporter, artifacts, "local_ocr_click_result");
|
|
1740
2795
|
if (verification.unavailable) {
|
|
1741
2796
|
lastFailureReason = verification.reason;
|
|
1742
2797
|
break;
|
|
@@ -1765,7 +2820,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1765
2820
|
break;
|
|
1766
2821
|
}
|
|
1767
2822
|
lastFailureReason = validationReason || `O clique por OCR local em ${targetDescription} nao teve efeito confirmavel.`;
|
|
1768
|
-
await
|
|
2823
|
+
await reportActionProgress("OCR local nao confirmou o clique; vou tentar visão remota");
|
|
1769
2824
|
screenshotPath = await this.takeScreenshot();
|
|
1770
2825
|
}
|
|
1771
2826
|
else if (ocrClick.reason) {
|
|
@@ -1806,7 +2861,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1806
2861
|
}
|
|
1807
2862
|
continue;
|
|
1808
2863
|
}
|
|
1809
|
-
await
|
|
2864
|
+
await reportActionProgress(`Clicando em ${targetDescription}`);
|
|
1810
2865
|
const scaledX = width > 0 && originalWidth > 0 ? (location.x / width) * originalWidth : location.x;
|
|
1811
2866
|
const scaledY = height > 0 && originalHeight > 0 ? (location.y / height) * originalHeight : location.y;
|
|
1812
2867
|
await this.clickPoint(scaledX, scaledY);
|
|
@@ -1817,7 +2872,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1817
2872
|
strategy: "visual_locator",
|
|
1818
2873
|
};
|
|
1819
2874
|
if (verificationPrompt) {
|
|
1820
|
-
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent,
|
|
2875
|
+
const verification = await this.validateVisualClickWithVision(job.job_id, targetDescription, verificationPrompt, progressPercent, stepReporter, artifacts, "visual_click_result");
|
|
1821
2876
|
if (verification.unavailable) {
|
|
1822
2877
|
lastFailureReason = verification.reason;
|
|
1823
2878
|
break;
|
|
@@ -1843,19 +2898,22 @@ export class NativeMacOSJobExecutor {
|
|
|
1843
2898
|
if (!clickSucceeded) {
|
|
1844
2899
|
throw new Error(lastFailureReason || `Nao consegui concluir o clique visual para ${action.description}.`);
|
|
1845
2900
|
}
|
|
2901
|
+
appendActionArtifact("interaction_result", {
|
|
2902
|
+
summary: `Localizei e cliquei em ${action.description}.`,
|
|
2903
|
+
});
|
|
1846
2904
|
continue;
|
|
1847
2905
|
}
|
|
1848
2906
|
if (action.type === "drag_visual_target") {
|
|
1849
2907
|
const dragApp = await this.resolveLikelyBrowserApp(action.app);
|
|
1850
2908
|
if (dragApp) {
|
|
1851
|
-
await
|
|
2909
|
+
await reportActionProgress(`Trazendo ${dragApp} para frente antes do arraste`);
|
|
1852
2910
|
await this.focusApp(dragApp);
|
|
1853
2911
|
}
|
|
1854
2912
|
else if (action.app) {
|
|
1855
|
-
await
|
|
2913
|
+
await reportActionProgress(`Trazendo ${action.app} para frente antes do arraste`);
|
|
1856
2914
|
await this.focusApp(action.app);
|
|
1857
2915
|
}
|
|
1858
|
-
await
|
|
2916
|
+
await reportActionProgress(`Capturando a tela para localizar ${action.source_description} e ${action.target_description}`);
|
|
1859
2917
|
const screenshotPath = await this.takeScreenshot();
|
|
1860
2918
|
const sourcePoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.source_description, artifacts, "drag_source");
|
|
1861
2919
|
const targetPoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.target_description, artifacts, "drag_target");
|
|
@@ -1865,18 +2923,29 @@ export class NativeMacOSJobExecutor {
|
|
|
1865
2923
|
if (!targetPoint) {
|
|
1866
2924
|
throw new Error(`Nao consegui localizar ${action.target_description} com confianca suficiente para concluir o arraste.`);
|
|
1867
2925
|
}
|
|
1868
|
-
await
|
|
2926
|
+
await reportActionProgress(`Arrastando ${action.source_description} para ${action.target_description}`);
|
|
1869
2927
|
await this.dragPoint(sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y);
|
|
1870
2928
|
resultPayload.last_drag = {
|
|
1871
2929
|
source: sourcePoint,
|
|
1872
2930
|
target: targetPoint,
|
|
1873
2931
|
};
|
|
2932
|
+
appendActionArtifact("interaction_result", {
|
|
2933
|
+
summary: `Arrastei ${action.source_description} para ${action.target_description}.`,
|
|
2934
|
+
});
|
|
1874
2935
|
completionNotes.push(`Arrastei ${action.source_description} para ${action.target_description}.`);
|
|
1875
2936
|
continue;
|
|
1876
2937
|
}
|
|
1877
|
-
await
|
|
2938
|
+
await reportActionProgress(`Abrindo ${action.url}${action.app ? ` em ${action.app}` : ""}`);
|
|
1878
2939
|
await this.openUrl(action.url, action.app);
|
|
1879
2940
|
await delay(1200);
|
|
2941
|
+
resultPayload.last_navigation = {
|
|
2942
|
+
url: action.url,
|
|
2943
|
+
app: action.app || null,
|
|
2944
|
+
};
|
|
2945
|
+
appendActionArtifact("navigation_result", {
|
|
2946
|
+
summary: `${humanizeUrl(action.url)} foi aberto${action.app ? ` em ${action.app}` : ""}.`,
|
|
2947
|
+
path: action.url,
|
|
2948
|
+
});
|
|
1880
2949
|
completionNotes.push(`${humanizeUrl(action.url)} foi aberto${action.app ? ` em ${action.app}` : ""}.`);
|
|
1881
2950
|
}
|
|
1882
2951
|
const summary = completionNotes.length > 0
|
|
@@ -1884,8 +2953,47 @@ export class NativeMacOSJobExecutor {
|
|
|
1884
2953
|
: (actions.length === 1
|
|
1885
2954
|
? this.describeAction(actions[0])
|
|
1886
2955
|
: `${actions.length} ações executadas no macOS`);
|
|
2956
|
+
attachWorkspaceMemory("completed");
|
|
2957
|
+
if (hookTrace.length > 0) {
|
|
2958
|
+
artifacts.push({
|
|
2959
|
+
id: `runtime_hook_trace.${job.job_id}`,
|
|
2960
|
+
kind: "runtime_hook_trace",
|
|
2961
|
+
summary: `Hook trace do runtime com ${hookTrace.length} eventos.`,
|
|
2962
|
+
metadata: {
|
|
2963
|
+
event_count: hookTrace.length,
|
|
2964
|
+
graph_id: runtimeManifest.graphId || undefined,
|
|
2965
|
+
},
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
1887
2968
|
resultPayload.summary = summary;
|
|
1888
|
-
await reporter.completed(resultPayload
|
|
2969
|
+
await reporter.completed(resultPayload, {
|
|
2970
|
+
stepId: runtimeManifest.finalizeStepId,
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
2973
|
+
catch (error) {
|
|
2974
|
+
if (error instanceof JobCancelledError) {
|
|
2975
|
+
throw error;
|
|
2976
|
+
}
|
|
2977
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2978
|
+
attachWorkspaceMemory("failed");
|
|
2979
|
+
if (hookTrace.length > 0 && !artifacts.some((artifact) => artifact.id === `runtime_hook_trace.${job.job_id}`)) {
|
|
2980
|
+
artifacts.push({
|
|
2981
|
+
id: `runtime_hook_trace.${job.job_id}`,
|
|
2982
|
+
kind: "runtime_hook_trace",
|
|
2983
|
+
summary: `Hook trace do runtime com ${hookTrace.length} eventos.`,
|
|
2984
|
+
metadata: {
|
|
2985
|
+
event_count: hookTrace.length,
|
|
2986
|
+
graph_id: runtimeManifest.graphId || undefined,
|
|
2987
|
+
},
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
await reporter.failed(detail || "Otto Bridge native-macos failed", {
|
|
2991
|
+
...resultPayload,
|
|
2992
|
+
summary: String(resultPayload.summary || detail || "").trim() || undefined,
|
|
2993
|
+
}, {
|
|
2994
|
+
stepId: currentActionStepId || runtimeManifest.executionStepId,
|
|
2995
|
+
});
|
|
2996
|
+
return;
|
|
1889
2997
|
}
|
|
1890
2998
|
finally {
|
|
1891
2999
|
this.cancelledJobs.delete(job.job_id);
|
|
@@ -2376,7 +3484,10 @@ return appNames as text
|
|
|
2376
3484
|
status.summary = this.buildAppStatusSummary(status);
|
|
2377
3485
|
return status;
|
|
2378
3486
|
}
|
|
2379
|
-
async resolveFilesystemInspectPath(targetPath) {
|
|
3487
|
+
async resolveFilesystemInspectPath(targetPath, workspaceContext) {
|
|
3488
|
+
if (workspaceContext) {
|
|
3489
|
+
return assertPathInsideWorkspace(workspaceContext, targetPath);
|
|
3490
|
+
}
|
|
2380
3491
|
const expanded = expandUserPath(targetPath);
|
|
2381
3492
|
try {
|
|
2382
3493
|
await stat(expanded);
|
|
@@ -2403,8 +3514,8 @@ return appNames as text
|
|
|
2403
3514
|
return 0;
|
|
2404
3515
|
}
|
|
2405
3516
|
}
|
|
2406
|
-
async inspectFilesystemPath(targetPath, includeChildren = true, includePreview = false, limit = 8) {
|
|
2407
|
-
const resolved = await this.resolveFilesystemInspectPath(targetPath);
|
|
3517
|
+
async inspectFilesystemPath(targetPath, includeChildren = true, includePreview = false, limit = 8, workspaceContext) {
|
|
3518
|
+
const resolved = await this.resolveFilesystemInspectPath(targetPath, workspaceContext);
|
|
2408
3519
|
const entryStat = await stat(resolved);
|
|
2409
3520
|
const itemName = path.basename(resolved) || resolved;
|
|
2410
3521
|
if (entryStat.isDirectory()) {
|
|
@@ -5140,8 +6251,8 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5140
6251
|
}
|
|
5141
6252
|
return clipTextPreview(loaded.content || "(arquivo vazio)", maxChars);
|
|
5142
6253
|
}
|
|
5143
|
-
async readLocalFileSnapshot(filePath, chunkSizeChars = 4000) {
|
|
5144
|
-
const resolved = await this.resolveReadableFilePath(filePath);
|
|
6254
|
+
async readLocalFileSnapshot(filePath, chunkSizeChars = 4000, workspaceContext) {
|
|
6255
|
+
const resolved = await this.resolveReadableFilePath(filePath, workspaceContext);
|
|
5145
6256
|
const entryStat = await stat(resolved);
|
|
5146
6257
|
const loaded = await this.loadReadableFileContent(resolved);
|
|
5147
6258
|
const fileName = path.basename(resolved) || resolved;
|
|
@@ -5179,7 +6290,10 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5179
6290
|
summary: `Li ${filePath} por completo (${content.length} caracteres em ${chunks.length} bloco${chunks.length === 1 ? "" : "s"}).`,
|
|
5180
6291
|
};
|
|
5181
6292
|
}
|
|
5182
|
-
async resolveTrashTargetPath(targetPath) {
|
|
6293
|
+
async resolveTrashTargetPath(targetPath, workspaceContext) {
|
|
6294
|
+
if (workspaceContext) {
|
|
6295
|
+
return assertPathInsideWorkspace(workspaceContext, targetPath);
|
|
6296
|
+
}
|
|
5183
6297
|
const resolved = expandUserPath(targetPath);
|
|
5184
6298
|
try {
|
|
5185
6299
|
await stat(resolved);
|
|
@@ -5268,8 +6382,8 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5268
6382
|
}
|
|
5269
6383
|
}
|
|
5270
6384
|
}
|
|
5271
|
-
async movePathToTrashSnapshot(targetPath) {
|
|
5272
|
-
const resolved = await this.resolveTrashTargetPath(targetPath);
|
|
6385
|
+
async movePathToTrashSnapshot(targetPath, workspaceContext) {
|
|
6386
|
+
const resolved = await this.resolveTrashTargetPath(targetPath, workspaceContext);
|
|
5273
6387
|
const entryStat = await stat(resolved);
|
|
5274
6388
|
const trashDir = path.join(os.homedir(), ".Trash");
|
|
5275
6389
|
await mkdir(trashDir, { recursive: true });
|
|
@@ -5293,39 +6407,55 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5293
6407
|
summary: `${kind === "directory" ? "Mandei a pasta" : kind === "file" ? "Mandei o arquivo" : "Mandei o item"} ${name} para a Lixeira.`,
|
|
5294
6408
|
};
|
|
5295
6409
|
}
|
|
5296
|
-
async resolveWritableTextFilePath(targetPath, filename) {
|
|
5297
|
-
const expanded =
|
|
6410
|
+
async resolveWritableTextFilePath(targetPath, filename, workspaceContext) {
|
|
6411
|
+
const expanded = workspaceContext
|
|
6412
|
+
? expandUserPathLike(targetPath, workspaceContext.defaultCwd)
|
|
6413
|
+
: expandUserPath(targetPath);
|
|
5298
6414
|
const requestedFilename = filename ? sanitizeFileName(filename) : null;
|
|
5299
6415
|
if (requestedFilename) {
|
|
5300
6416
|
try {
|
|
5301
6417
|
const existingStat = await stat(expanded);
|
|
5302
6418
|
if (existingStat.isDirectory()) {
|
|
5303
|
-
|
|
6419
|
+
const resolvedDirectoryPath = path.join(expanded, requestedFilename);
|
|
6420
|
+
return workspaceContext
|
|
6421
|
+
? assertPathInsideWorkspace(workspaceContext, resolvedDirectoryPath)
|
|
6422
|
+
: resolvedDirectoryPath;
|
|
5304
6423
|
}
|
|
5305
6424
|
}
|
|
5306
6425
|
catch {
|
|
5307
6426
|
// Continue below and treat the target as a direct file path.
|
|
5308
6427
|
}
|
|
5309
6428
|
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
5310
|
-
|
|
6429
|
+
const resolvedDirectoryPath = path.join(expanded, requestedFilename);
|
|
6430
|
+
return workspaceContext
|
|
6431
|
+
? assertPathInsideWorkspace(workspaceContext, resolvedDirectoryPath)
|
|
6432
|
+
: resolvedDirectoryPath;
|
|
5311
6433
|
}
|
|
5312
6434
|
}
|
|
5313
6435
|
try {
|
|
5314
6436
|
const existingStat = await stat(expanded);
|
|
5315
6437
|
if (existingStat.isDirectory()) {
|
|
5316
|
-
|
|
6438
|
+
const fallbackDirectoryPath = path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
6439
|
+
return workspaceContext
|
|
6440
|
+
? assertPathInsideWorkspace(workspaceContext, fallbackDirectoryPath)
|
|
6441
|
+
: fallbackDirectoryPath;
|
|
5317
6442
|
}
|
|
5318
6443
|
}
|
|
5319
6444
|
catch {
|
|
5320
6445
|
// Continue below and treat the target as a direct file path.
|
|
5321
6446
|
}
|
|
5322
6447
|
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
5323
|
-
|
|
6448
|
+
const fallbackDirectoryPath = path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
6449
|
+
return workspaceContext
|
|
6450
|
+
? assertPathInsideWorkspace(workspaceContext, fallbackDirectoryPath)
|
|
6451
|
+
: fallbackDirectoryPath;
|
|
5324
6452
|
}
|
|
5325
|
-
return
|
|
6453
|
+
return workspaceContext
|
|
6454
|
+
? assertPathInsideWorkspace(workspaceContext, expanded)
|
|
6455
|
+
: expanded;
|
|
5326
6456
|
}
|
|
5327
|
-
async writeTextFileSnapshot(targetPath, text, filename, append = false, source) {
|
|
5328
|
-
const resolved = await this.resolveWritableTextFilePath(targetPath, filename);
|
|
6457
|
+
async writeTextFileSnapshot(targetPath, text, filename, append = false, source, workspaceContext) {
|
|
6458
|
+
const resolved = await this.resolveWritableTextFilePath(targetPath, filename, workspaceContext);
|
|
5329
6459
|
const parentDir = path.dirname(resolved);
|
|
5330
6460
|
await mkdir(parentDir, { recursive: true });
|
|
5331
6461
|
await writeFile(resolved, text, {
|
|
@@ -5350,6 +6480,281 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5350
6480
|
summary: `${append ? "Atualizei" : "Escrevi"} ${String(text || "").length} caractere${String(text || "").length === 1 ? "" : "s"} em ${resolved}.`,
|
|
5351
6481
|
};
|
|
5352
6482
|
}
|
|
6483
|
+
async writeJsonFileSnapshot(targetPath, data, filename, pretty = true, workspaceContext) {
|
|
6484
|
+
const serialized = JSON.stringify(data, null, pretty ? 2 : 0);
|
|
6485
|
+
if (typeof serialized !== "string") {
|
|
6486
|
+
throw new Error("Nao consegui serializar o JSON pedido para gravar no arquivo local.");
|
|
6487
|
+
}
|
|
6488
|
+
const resolved = await this.resolveWritableTextFilePath(targetPath, filename, workspaceContext);
|
|
6489
|
+
const parentDir = path.dirname(resolved);
|
|
6490
|
+
await mkdir(parentDir, { recursive: true });
|
|
6491
|
+
await writeFile(resolved, serialized, { encoding: "utf8", flag: "w" });
|
|
6492
|
+
const entryStat = await stat(resolved);
|
|
6493
|
+
const name = path.basename(resolved) || resolved;
|
|
6494
|
+
return {
|
|
6495
|
+
captured_at: new Date().toISOString(),
|
|
6496
|
+
path: targetPath,
|
|
6497
|
+
resolved_path: resolved,
|
|
6498
|
+
name,
|
|
6499
|
+
mime_type: "application/json; charset=utf-8",
|
|
6500
|
+
size_bytes: entryStat.size,
|
|
6501
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
6502
|
+
content_char_count: serialized.length,
|
|
6503
|
+
content_preview: clipTextPreview(serialized || "{}", 240),
|
|
6504
|
+
pretty,
|
|
6505
|
+
summary: `Gravei ${serialized.length} caractere${serialized.length === 1 ? "" : "s"} de JSON em ${resolved}.`,
|
|
6506
|
+
};
|
|
6507
|
+
}
|
|
6508
|
+
resolvePatchTargetPath(targetPath, resolvedCwd, workspaceContext) {
|
|
6509
|
+
return workspaceContext
|
|
6510
|
+
? assertPathInsideWorkspace(workspaceContext, targetPath, { baseCwd: resolvedCwd })
|
|
6511
|
+
: expandUserPathLike(targetPath, resolvedCwd);
|
|
6512
|
+
}
|
|
6513
|
+
countTextLines(value) {
|
|
6514
|
+
const normalized = String(value || "").replace(/\r\n/g, "\n");
|
|
6515
|
+
if (!normalized) {
|
|
6516
|
+
return 0;
|
|
6517
|
+
}
|
|
6518
|
+
const trimmed = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
6519
|
+
return trimmed ? trimmed.split("\n").length : 0;
|
|
6520
|
+
}
|
|
6521
|
+
async applyPatchOperationSnapshot(operation, resolvedCwd, workspaceContext) {
|
|
6522
|
+
if (operation.type === "add") {
|
|
6523
|
+
const resolvedPath = this.resolvePatchTargetPath(operation.path, resolvedCwd, workspaceContext);
|
|
6524
|
+
try {
|
|
6525
|
+
await stat(resolvedPath);
|
|
6526
|
+
throw new Error(`O apply_patch nao pode adicionar ${resolvedPath} porque o arquivo ja existe.`);
|
|
6527
|
+
}
|
|
6528
|
+
catch (error) {
|
|
6529
|
+
if (error?.code !== "ENOENT") {
|
|
6530
|
+
throw error;
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
6534
|
+
const fileContent = operation.content_lines.length > 0 ? `${operation.content_lines.join("\n")}\n` : "";
|
|
6535
|
+
await writeFile(resolvedPath, fileContent, "utf8");
|
|
6536
|
+
return {
|
|
6537
|
+
path: operation.path,
|
|
6538
|
+
resolved_path: resolvedPath,
|
|
6539
|
+
status: "added",
|
|
6540
|
+
added_line_count: operation.content_lines.length,
|
|
6541
|
+
deleted_line_count: 0,
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
if (operation.type === "delete") {
|
|
6545
|
+
const resolvedPath = this.resolvePatchTargetPath(operation.path, resolvedCwd, workspaceContext);
|
|
6546
|
+
const entryStat = await stat(resolvedPath);
|
|
6547
|
+
if (entryStat.isDirectory()) {
|
|
6548
|
+
throw new Error(`O apply_patch nao suporta remover pastas: ${resolvedPath}.`);
|
|
6549
|
+
}
|
|
6550
|
+
let deletedLineCount = 0;
|
|
6551
|
+
try {
|
|
6552
|
+
deletedLineCount = this.countTextLines(await readFile(resolvedPath, "utf8"));
|
|
6553
|
+
}
|
|
6554
|
+
catch {
|
|
6555
|
+
deletedLineCount = 0;
|
|
6556
|
+
}
|
|
6557
|
+
await unlink(resolvedPath);
|
|
6558
|
+
return {
|
|
6559
|
+
path: operation.path,
|
|
6560
|
+
resolved_path: resolvedPath,
|
|
6561
|
+
status: "deleted",
|
|
6562
|
+
added_line_count: 0,
|
|
6563
|
+
deleted_line_count: deletedLineCount,
|
|
6564
|
+
};
|
|
6565
|
+
}
|
|
6566
|
+
const resolvedSourcePath = this.resolvePatchTargetPath(operation.path, resolvedCwd, workspaceContext);
|
|
6567
|
+
const originalText = await readFile(resolvedSourcePath, "utf8");
|
|
6568
|
+
const applied = applyStructuredUpdateToText(originalText, operation);
|
|
6569
|
+
const resolvedDestinationPath = operation.move_to
|
|
6570
|
+
? this.resolvePatchTargetPath(operation.move_to, resolvedCwd, workspaceContext)
|
|
6571
|
+
: resolvedSourcePath;
|
|
6572
|
+
if (operation.move_to && resolvedDestinationPath !== resolvedSourcePath) {
|
|
6573
|
+
try {
|
|
6574
|
+
await stat(resolvedDestinationPath);
|
|
6575
|
+
throw new Error(`O apply_patch nao pode mover para ${resolvedDestinationPath} porque o destino ja existe.`);
|
|
6576
|
+
}
|
|
6577
|
+
catch (error) {
|
|
6578
|
+
if (error?.code !== "ENOENT") {
|
|
6579
|
+
throw error;
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
await mkdir(path.dirname(resolvedDestinationPath), { recursive: true });
|
|
6583
|
+
await writeFile(resolvedDestinationPath, applied.text, "utf8");
|
|
6584
|
+
await unlink(resolvedSourcePath);
|
|
6585
|
+
return {
|
|
6586
|
+
path: operation.move_to || operation.path,
|
|
6587
|
+
resolved_path: resolvedDestinationPath,
|
|
6588
|
+
status: "moved",
|
|
6589
|
+
previous_path: operation.path,
|
|
6590
|
+
resolved_previous_path: resolvedSourcePath,
|
|
6591
|
+
added_line_count: applied.addedLineCount,
|
|
6592
|
+
deleted_line_count: applied.deletedLineCount,
|
|
6593
|
+
};
|
|
6594
|
+
}
|
|
6595
|
+
await writeFile(resolvedSourcePath, applied.text, "utf8");
|
|
6596
|
+
return {
|
|
6597
|
+
path: operation.path,
|
|
6598
|
+
resolved_path: resolvedSourcePath,
|
|
6599
|
+
status: "updated",
|
|
6600
|
+
added_line_count: applied.addedLineCount,
|
|
6601
|
+
deleted_line_count: applied.deletedLineCount,
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
async applyPatchSnapshot(patchText, cwd, workspaceContext) {
|
|
6605
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd, workspaceContext);
|
|
6606
|
+
const parsed = parseStructuredPatch(patchText);
|
|
6607
|
+
if (parsed.operations.length === 0) {
|
|
6608
|
+
throw new Error("O apply_patch nao recebeu nenhuma operacao valida.");
|
|
6609
|
+
}
|
|
6610
|
+
const files = [];
|
|
6611
|
+
for (const operation of parsed.operations) {
|
|
6612
|
+
files.push(await this.applyPatchOperationSnapshot(operation, resolvedCwd, workspaceContext));
|
|
6613
|
+
}
|
|
6614
|
+
const addedCount = files.filter((item) => item.status === "added").length;
|
|
6615
|
+
const updatedCount = files.filter((item) => item.status === "updated").length;
|
|
6616
|
+
const deletedCount = files.filter((item) => item.status === "deleted").length;
|
|
6617
|
+
const movedCount = files.filter((item) => item.status === "moved").length;
|
|
6618
|
+
const changedFiles = uniqueStrings(files.map((item) => item.path));
|
|
6619
|
+
const changedFileCount = files.length;
|
|
6620
|
+
const summary = changedFileCount === 0
|
|
6621
|
+
? `O patch em ${resolvedCwd} nao gerou mudancas persistidas.`
|
|
6622
|
+
: `Apliquei um patch em ${resolvedCwd} com ${changedFileCount} arquivo${changedFileCount === 1 ? "" : "s"} alterado${changedFileCount === 1 ? "" : "s"} (${addedCount} adicionados, ${updatedCount} atualizados, ${deletedCount} removidos, ${movedCount} movidos).`;
|
|
6623
|
+
return {
|
|
6624
|
+
captured_at: new Date().toISOString(),
|
|
6625
|
+
cwd,
|
|
6626
|
+
resolved_cwd: resolvedCwd,
|
|
6627
|
+
patch_char_count: parsed.patch_char_count,
|
|
6628
|
+
operation_count: parsed.operations.length,
|
|
6629
|
+
changed_file_count: changedFileCount,
|
|
6630
|
+
added_count: addedCount,
|
|
6631
|
+
updated_count: updatedCount,
|
|
6632
|
+
deleted_count: deletedCount,
|
|
6633
|
+
moved_count: movedCount,
|
|
6634
|
+
changed_files: changedFiles,
|
|
6635
|
+
files,
|
|
6636
|
+
summary,
|
|
6637
|
+
};
|
|
6638
|
+
}
|
|
6639
|
+
async createDirectorySnapshot(targetPath, createParents = true, workspaceContext) {
|
|
6640
|
+
const resolved = workspaceContext
|
|
6641
|
+
? assertPathInsideWorkspace(workspaceContext, targetPath)
|
|
6642
|
+
: expandUserPath(targetPath);
|
|
6643
|
+
let existedBefore = false;
|
|
6644
|
+
try {
|
|
6645
|
+
const existingStat = await stat(resolved);
|
|
6646
|
+
if (!existingStat.isDirectory()) {
|
|
6647
|
+
throw new Error(`Ja existe um item em ${resolved}, mas ele nao e uma pasta.`);
|
|
6648
|
+
}
|
|
6649
|
+
existedBefore = true;
|
|
6650
|
+
}
|
|
6651
|
+
catch (error) {
|
|
6652
|
+
if (error?.code !== "ENOENT") {
|
|
6653
|
+
throw error;
|
|
6654
|
+
}
|
|
6655
|
+
}
|
|
6656
|
+
await mkdir(resolved, { recursive: createParents });
|
|
6657
|
+
const directoryStat = await stat(resolved);
|
|
6658
|
+
return {
|
|
6659
|
+
captured_at: new Date().toISOString(),
|
|
6660
|
+
path: targetPath,
|
|
6661
|
+
resolved_path: resolved,
|
|
6662
|
+
name: path.basename(resolved) || resolved,
|
|
6663
|
+
created: !existedBefore,
|
|
6664
|
+
existed_before: existedBefore,
|
|
6665
|
+
modified_at: directoryStat.mtime.toISOString(),
|
|
6666
|
+
summary: existedBefore
|
|
6667
|
+
? `A pasta ${resolved} ja existia e ficou pronta para uso.`
|
|
6668
|
+
: `Criei a pasta ${resolved} no macOS.`,
|
|
6669
|
+
};
|
|
6670
|
+
}
|
|
6671
|
+
async resolveMoveDestinationPath(sourceResolvedPath, destinationPath, workspaceContext) {
|
|
6672
|
+
const expanded = workspaceContext
|
|
6673
|
+
? assertPathInsideWorkspace(workspaceContext, destinationPath)
|
|
6674
|
+
: expandUserPath(destinationPath);
|
|
6675
|
+
try {
|
|
6676
|
+
const destinationStat = await stat(expanded);
|
|
6677
|
+
if (destinationStat.isDirectory()) {
|
|
6678
|
+
const nestedDestination = path.join(expanded, path.basename(sourceResolvedPath));
|
|
6679
|
+
return workspaceContext
|
|
6680
|
+
? assertPathInsideWorkspace(workspaceContext, nestedDestination)
|
|
6681
|
+
: nestedDestination;
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
catch {
|
|
6685
|
+
// Continue below.
|
|
6686
|
+
}
|
|
6687
|
+
if (String(destinationPath || "").trimEnd().endsWith(path.sep)) {
|
|
6688
|
+
const nestedDestination = path.join(expanded, path.basename(sourceResolvedPath));
|
|
6689
|
+
return workspaceContext
|
|
6690
|
+
? assertPathInsideWorkspace(workspaceContext, nestedDestination)
|
|
6691
|
+
: nestedDestination;
|
|
6692
|
+
}
|
|
6693
|
+
return expanded;
|
|
6694
|
+
}
|
|
6695
|
+
async moveLocalPathSnapshot(sourcePath, destinationPath, overwrite = false, workspaceContext) {
|
|
6696
|
+
const resolvedSourcePath = workspaceContext
|
|
6697
|
+
? assertPathInsideWorkspace(workspaceContext, sourcePath)
|
|
6698
|
+
: expandUserPath(sourcePath);
|
|
6699
|
+
const sourceStat = await stat(resolvedSourcePath);
|
|
6700
|
+
const resolvedDestinationPath = await this.resolveMoveDestinationPath(resolvedSourcePath, destinationPath, workspaceContext);
|
|
6701
|
+
await mkdir(path.dirname(resolvedDestinationPath), { recursive: true });
|
|
6702
|
+
let overwritten = false;
|
|
6703
|
+
try {
|
|
6704
|
+
const destinationStat = await stat(resolvedDestinationPath);
|
|
6705
|
+
if (!overwrite) {
|
|
6706
|
+
throw new Error(`Ja existe um item em ${resolvedDestinationPath}. Defina overwrite=true se quiser substituir esse arquivo.`);
|
|
6707
|
+
}
|
|
6708
|
+
if (destinationStat.isDirectory()) {
|
|
6709
|
+
throw new Error(`Ja existe uma pasta em ${resolvedDestinationPath}.`);
|
|
6710
|
+
}
|
|
6711
|
+
await unlink(resolvedDestinationPath);
|
|
6712
|
+
overwritten = true;
|
|
6713
|
+
}
|
|
6714
|
+
catch (error) {
|
|
6715
|
+
if (error?.code !== "ENOENT") {
|
|
6716
|
+
throw error;
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
await rename(resolvedSourcePath, resolvedDestinationPath);
|
|
6720
|
+
const itemKind = sourceStat.isDirectory()
|
|
6721
|
+
? "directory"
|
|
6722
|
+
: sourceStat.isFile()
|
|
6723
|
+
? "file"
|
|
6724
|
+
: "other";
|
|
6725
|
+
return {
|
|
6726
|
+
captured_at: new Date().toISOString(),
|
|
6727
|
+
source_path: sourcePath,
|
|
6728
|
+
resolved_source_path: resolvedSourcePath,
|
|
6729
|
+
destination_path: destinationPath,
|
|
6730
|
+
resolved_destination_path: resolvedDestinationPath,
|
|
6731
|
+
name: path.basename(resolvedDestinationPath) || resolvedDestinationPath,
|
|
6732
|
+
item_kind: itemKind,
|
|
6733
|
+
overwritten,
|
|
6734
|
+
summary: `Movi ${itemKind === "directory" ? "a pasta" : "o item"} ${path.basename(resolvedSourcePath)} para ${resolvedDestinationPath}.`,
|
|
6735
|
+
};
|
|
6736
|
+
}
|
|
6737
|
+
async deleteLocalFileSnapshot(targetPath, workspaceContext) {
|
|
6738
|
+
const resolvedPath = workspaceContext
|
|
6739
|
+
? assertPathInsideWorkspace(workspaceContext, targetPath)
|
|
6740
|
+
: expandUserPath(targetPath);
|
|
6741
|
+
const entryStat = await stat(resolvedPath);
|
|
6742
|
+
if (entryStat.isDirectory()) {
|
|
6743
|
+
throw new Error("delete_file so suporta arquivos por enquanto. Use trash_path para pastas.");
|
|
6744
|
+
}
|
|
6745
|
+
const itemKind = entryStat.isFile() ? "file" : "other";
|
|
6746
|
+
await unlink(resolvedPath);
|
|
6747
|
+
return {
|
|
6748
|
+
captured_at: new Date().toISOString(),
|
|
6749
|
+
path: targetPath,
|
|
6750
|
+
resolved_path: resolvedPath,
|
|
6751
|
+
name: path.basename(resolvedPath) || resolvedPath,
|
|
6752
|
+
item_kind: itemKind,
|
|
6753
|
+
size_bytes: entryStat.size,
|
|
6754
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
6755
|
+
summary: `Apaguei o arquivo ${resolvedPath} no macOS.`,
|
|
6756
|
+
};
|
|
6757
|
+
}
|
|
5353
6758
|
resolveWriteTextFileContent(action) {
|
|
5354
6759
|
const explicitText = [action.text, action.content, action.body]
|
|
5355
6760
|
.map((value) => String(value || "").trim())
|
|
@@ -5366,7 +6771,10 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5366
6771
|
}
|
|
5367
6772
|
return null;
|
|
5368
6773
|
}
|
|
5369
|
-
async resolveReadableFilePath(filePath) {
|
|
6774
|
+
async resolveReadableFilePath(filePath, workspaceContext) {
|
|
6775
|
+
if (workspaceContext) {
|
|
6776
|
+
return assertPathInsideWorkspace(workspaceContext, filePath);
|
|
6777
|
+
}
|
|
5370
6778
|
const resolved = expandUserPath(filePath);
|
|
5371
6779
|
try {
|
|
5372
6780
|
await stat(resolved);
|
|
@@ -5435,8 +6843,10 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5435
6843
|
}
|
|
5436
6844
|
return null;
|
|
5437
6845
|
}
|
|
5438
|
-
async listLocalFilesSnapshot(directoryPath, limit) {
|
|
5439
|
-
const resolved =
|
|
6846
|
+
async listLocalFilesSnapshot(directoryPath, limit, workspaceContext) {
|
|
6847
|
+
const resolved = workspaceContext
|
|
6848
|
+
? assertPathInsideWorkspace(workspaceContext, directoryPath)
|
|
6849
|
+
: expandUserPath(directoryPath);
|
|
5440
6850
|
const allEntries = await readdir(resolved, { withFileTypes: true });
|
|
5441
6851
|
const sortedEntries = allEntries.sort((left, right) => {
|
|
5442
6852
|
if (left.isDirectory() !== right.isDirectory()) {
|
|
@@ -5485,8 +6895,10 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5485
6895
|
summary,
|
|
5486
6896
|
};
|
|
5487
6897
|
}
|
|
5488
|
-
async countLocalFiles(directoryPath, extensions, recursive = true) {
|
|
5489
|
-
const resolved =
|
|
6898
|
+
async countLocalFiles(directoryPath, extensions, recursive = true, workspaceContext) {
|
|
6899
|
+
const resolved = workspaceContext
|
|
6900
|
+
? assertPathInsideWorkspace(workspaceContext, directoryPath)
|
|
6901
|
+
: expandUserPath(directoryPath);
|
|
5490
6902
|
const normalizedExtensions = Array.from(new Set((extensions || [])
|
|
5491
6903
|
.map((extension) => String(extension || "").trim().toLowerCase().replace(/^\./, ""))
|
|
5492
6904
|
.filter(Boolean)));
|
|
@@ -5761,11 +7173,1174 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5761
7173
|
status.summary = this.buildSystemStatusSummary(status);
|
|
5762
7174
|
return status;
|
|
5763
7175
|
}
|
|
5764
|
-
|
|
7176
|
+
resolveWorkspaceExecutionCwd(cwd, workspaceContext) {
|
|
7177
|
+
return workspaceContext
|
|
7178
|
+
? assertCwdInsideWorkspace(workspaceContext, cwd)
|
|
7179
|
+
: (cwd ? expandUserPath(cwd) : process.cwd());
|
|
7180
|
+
}
|
|
7181
|
+
summarizeGitCommandError(result, fallback) {
|
|
7182
|
+
const combined = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n").trim();
|
|
7183
|
+
return clipText(combined || fallback, 4_000);
|
|
7184
|
+
}
|
|
7185
|
+
async probeGitRepository(cwd, workspaceContext) {
|
|
7186
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd, workspaceContext);
|
|
7187
|
+
const capturedAt = new Date().toISOString();
|
|
7188
|
+
const repoProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--show-toplevel"], {
|
|
7189
|
+
cwd: resolvedCwd,
|
|
7190
|
+
allowNonZeroExit: true,
|
|
7191
|
+
});
|
|
7192
|
+
if (repoProbe.exitCode !== 0) {
|
|
7193
|
+
return {
|
|
7194
|
+
capturedAt,
|
|
7195
|
+
resolvedCwd,
|
|
7196
|
+
isRepo: false,
|
|
7197
|
+
};
|
|
7198
|
+
}
|
|
7199
|
+
const repoRoot = repoProbe.stdout.trim() || resolvedCwd;
|
|
7200
|
+
const branchProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
7201
|
+
cwd: resolvedCwd,
|
|
7202
|
+
allowNonZeroExit: true,
|
|
7203
|
+
});
|
|
7204
|
+
const trackingProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
7205
|
+
cwd: resolvedCwd,
|
|
7206
|
+
allowNonZeroExit: true,
|
|
7207
|
+
});
|
|
7208
|
+
return {
|
|
7209
|
+
capturedAt,
|
|
7210
|
+
resolvedCwd,
|
|
7211
|
+
isRepo: true,
|
|
7212
|
+
repoRoot,
|
|
7213
|
+
currentBranch: branchProbe.exitCode === 0 ? (branchProbe.stdout.trim() || undefined) : undefined,
|
|
7214
|
+
trackingRef: trackingProbe.exitCode === 0 ? (trackingProbe.stdout.trim() || undefined) : undefined,
|
|
7215
|
+
};
|
|
7216
|
+
}
|
|
7217
|
+
async pathExists(candidatePath) {
|
|
7218
|
+
try {
|
|
7219
|
+
await stat(candidatePath);
|
|
7220
|
+
return true;
|
|
7221
|
+
}
|
|
7222
|
+
catch {
|
|
7223
|
+
return false;
|
|
7224
|
+
}
|
|
7225
|
+
}
|
|
7226
|
+
parseGitFetchUpdatedRefs(output) {
|
|
7227
|
+
const updatedRefs = [];
|
|
7228
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
7229
|
+
const line = rawLine.trim();
|
|
7230
|
+
if (!line || !line.includes("->")) {
|
|
7231
|
+
continue;
|
|
7232
|
+
}
|
|
7233
|
+
const refMatch = line.match(/->\s+(.+)$/);
|
|
7234
|
+
if (refMatch?.[1]) {
|
|
7235
|
+
updatedRefs.push(refMatch[1].trim());
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
return uniqueStrings(updatedRefs);
|
|
7239
|
+
}
|
|
7240
|
+
async resolveWorkspacePackageManager(resolvedCwd, workspaceContext) {
|
|
7241
|
+
const manifestPackageManagers = uniqueStrings(workspaceContext?.repoManifest?.package_managers || []);
|
|
7242
|
+
for (const packageManager of PACKAGE_MANAGER_PRIORITY) {
|
|
7243
|
+
if (manifestPackageManagers.includes(packageManager)) {
|
|
7244
|
+
return packageManager;
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
if (await this.pathExists(path.join(resolvedCwd, "pnpm-lock.yaml"))) {
|
|
7248
|
+
return "pnpm";
|
|
7249
|
+
}
|
|
7250
|
+
if (await this.pathExists(path.join(resolvedCwd, "yarn.lock"))) {
|
|
7251
|
+
return "yarn";
|
|
7252
|
+
}
|
|
7253
|
+
if (await this.pathExists(path.join(resolvedCwd, "bun.lockb"))) {
|
|
7254
|
+
return "bun";
|
|
7255
|
+
}
|
|
7256
|
+
if (await this.pathExists(path.join(resolvedCwd, "package-lock.json")) || await this.pathExists(path.join(resolvedCwd, "package.json"))) {
|
|
7257
|
+
return "npm";
|
|
7258
|
+
}
|
|
7259
|
+
return undefined;
|
|
7260
|
+
}
|
|
7261
|
+
detectWorkspaceValidationStacks(resolvedCwd, workspaceContext) {
|
|
7262
|
+
const detected = new Set();
|
|
7263
|
+
const manifestFiles = uniqueStrings(workspaceContext?.repoManifest?.manifest_files || []);
|
|
7264
|
+
const packageManagers = uniqueStrings(workspaceContext?.repoManifest?.package_managers || []);
|
|
7265
|
+
if (packageManagers.length > 0 || manifestFiles.includes("package.json")) {
|
|
7266
|
+
detected.add("node");
|
|
7267
|
+
}
|
|
7268
|
+
if (manifestFiles.includes("pyproject.toml") || manifestFiles.includes("requirements.txt")) {
|
|
7269
|
+
detected.add("python");
|
|
7270
|
+
}
|
|
7271
|
+
if (detected.size === 0) {
|
|
7272
|
+
detected.add("node");
|
|
7273
|
+
}
|
|
7274
|
+
return Array.from(detected);
|
|
7275
|
+
}
|
|
7276
|
+
resolveValidationLadderStages(manifest, resolvedCwd, workspaceContext) {
|
|
7277
|
+
const ladder = manifest?.validationLadder;
|
|
7278
|
+
if (!ladder || !Array.isArray(ladder.stages) || ladder.stages.length === 0) {
|
|
7279
|
+
return [];
|
|
7280
|
+
}
|
|
7281
|
+
const stacks = this.detectWorkspaceValidationStacks(resolvedCwd, workspaceContext);
|
|
7282
|
+
const selectedStages = ladder.stages
|
|
7283
|
+
.filter((stage) => {
|
|
7284
|
+
if (!stage.profile) {
|
|
7285
|
+
return false;
|
|
7286
|
+
}
|
|
7287
|
+
if (!stage.stack_tags || stage.stack_tags.length === 0) {
|
|
7288
|
+
return true;
|
|
7289
|
+
}
|
|
7290
|
+
return stage.stack_tags.some((tag) => stacks.includes(tag));
|
|
7291
|
+
})
|
|
7292
|
+
.sort((left, right) => (left.order || 0) - (right.order || 0))
|
|
7293
|
+
.map((stage) => ({
|
|
7294
|
+
stage_id: stage.stage_id,
|
|
7295
|
+
title: stage.title,
|
|
7296
|
+
profile: stage.profile,
|
|
7297
|
+
}));
|
|
7298
|
+
const seenProfiles = new Set();
|
|
7299
|
+
return selectedStages.filter((stage) => {
|
|
7300
|
+
if (seenProfiles.has(stage.profile)) {
|
|
7301
|
+
return false;
|
|
7302
|
+
}
|
|
7303
|
+
seenProfiles.add(stage.profile);
|
|
7304
|
+
return true;
|
|
7305
|
+
});
|
|
7306
|
+
}
|
|
7307
|
+
async resolveRunTestsCommand(command, profile, resolvedCwd, workspaceContext) {
|
|
7308
|
+
const explicitCommand = asString(command);
|
|
7309
|
+
if (explicitCommand) {
|
|
7310
|
+
return {
|
|
7311
|
+
command: explicitCommand,
|
|
7312
|
+
resolvedCommand: explicitCommand,
|
|
7313
|
+
profile,
|
|
7314
|
+
};
|
|
7315
|
+
}
|
|
7316
|
+
if (!profile) {
|
|
7317
|
+
throw new Error("Nenhum comando ou profile de validacao foi informado para run_tests.");
|
|
7318
|
+
}
|
|
7319
|
+
if (profile === "auto") {
|
|
7320
|
+
throw new Error("O profile auto precisa ser resolvido pela validation ladder antes de executar run_tests.");
|
|
7321
|
+
}
|
|
7322
|
+
if (profile === "pytest") {
|
|
7323
|
+
const localPytest = path.join(resolvedCwd, ".venv", "bin", "pytest");
|
|
7324
|
+
if (await this.pathExists(localPytest)) {
|
|
7325
|
+
return {
|
|
7326
|
+
command: "./.venv/bin/pytest -q",
|
|
7327
|
+
resolvedCommand: "./.venv/bin/pytest -q",
|
|
7328
|
+
profile,
|
|
7329
|
+
};
|
|
7330
|
+
}
|
|
7331
|
+
if (await this.pathExists(path.join(resolvedCwd, "pyproject.toml"))
|
|
7332
|
+
|| await this.pathExists(path.join(resolvedCwd, "pytest.ini"))
|
|
7333
|
+
|| await this.pathExists(path.join(resolvedCwd, "requirements.txt"))) {
|
|
7334
|
+
return {
|
|
7335
|
+
command: "python -m pytest -q",
|
|
7336
|
+
resolvedCommand: "python -m pytest -q",
|
|
7337
|
+
profile,
|
|
7338
|
+
};
|
|
7339
|
+
}
|
|
7340
|
+
return {
|
|
7341
|
+
command: "pytest -q",
|
|
7342
|
+
resolvedCommand: "pytest -q",
|
|
7343
|
+
profile,
|
|
7344
|
+
};
|
|
7345
|
+
}
|
|
7346
|
+
if (profile === "node_test") {
|
|
7347
|
+
const packageManager = await this.resolveWorkspacePackageManager(resolvedCwd, workspaceContext);
|
|
7348
|
+
if (!packageManager) {
|
|
7349
|
+
throw new Error(`Nao encontrei um package manager suportado para resolver o profile ${profile} em ${resolvedCwd}.`);
|
|
7350
|
+
}
|
|
7351
|
+
const resolvedCommand = packageManager === "yarn"
|
|
7352
|
+
? "yarn test"
|
|
7353
|
+
: packageManager === "bun"
|
|
7354
|
+
? "bun test"
|
|
7355
|
+
: `${packageManager} test`;
|
|
7356
|
+
return {
|
|
7357
|
+
command: resolvedCommand,
|
|
7358
|
+
resolvedCommand,
|
|
7359
|
+
profile,
|
|
7360
|
+
};
|
|
7361
|
+
}
|
|
7362
|
+
if (profile === "npm_test") {
|
|
7363
|
+
return { command: "npm test", resolvedCommand: "npm test", profile };
|
|
7364
|
+
}
|
|
7365
|
+
if (profile === "pnpm_test") {
|
|
7366
|
+
return { command: "pnpm test", resolvedCommand: "pnpm test", profile };
|
|
7367
|
+
}
|
|
7368
|
+
if (profile === "yarn_test") {
|
|
7369
|
+
return { command: "yarn test", resolvedCommand: "yarn test", profile };
|
|
7370
|
+
}
|
|
7371
|
+
if (profile === "bun_test") {
|
|
7372
|
+
return { command: "bun test", resolvedCommand: "bun test", profile };
|
|
7373
|
+
}
|
|
7374
|
+
if (profile === "typecheck") {
|
|
7375
|
+
const localTsc = path.join(resolvedCwd, "node_modules", ".bin", "tsc");
|
|
7376
|
+
if (await this.pathExists(localTsc) && await this.pathExists(path.join(resolvedCwd, "tsconfig.json"))) {
|
|
7377
|
+
return {
|
|
7378
|
+
command: "./node_modules/.bin/tsc --noEmit",
|
|
7379
|
+
resolvedCommand: "./node_modules/.bin/tsc --noEmit",
|
|
7380
|
+
profile,
|
|
7381
|
+
};
|
|
7382
|
+
}
|
|
7383
|
+
const packageManager = await this.resolveWorkspacePackageManager(resolvedCwd, workspaceContext);
|
|
7384
|
+
if (packageManager) {
|
|
7385
|
+
const resolvedCommand = packageManager === "yarn"
|
|
7386
|
+
? "yarn typecheck"
|
|
7387
|
+
: packageManager === "bun"
|
|
7388
|
+
? "bun run typecheck"
|
|
7389
|
+
: `${packageManager} run typecheck`;
|
|
7390
|
+
return {
|
|
7391
|
+
command: resolvedCommand,
|
|
7392
|
+
resolvedCommand,
|
|
7393
|
+
profile,
|
|
7394
|
+
};
|
|
7395
|
+
}
|
|
7396
|
+
throw new Error(`Nao encontrei um comando de typecheck suportado para ${resolvedCwd}.`);
|
|
7397
|
+
}
|
|
7398
|
+
const packageManager = await this.resolveWorkspacePackageManager(resolvedCwd, workspaceContext);
|
|
7399
|
+
if (!packageManager) {
|
|
7400
|
+
throw new Error(`Nao encontrei um package manager suportado para resolver o profile ${profile} em ${resolvedCwd}.`);
|
|
7401
|
+
}
|
|
7402
|
+
const scriptName = profile === "lint" ? "lint" : "build";
|
|
7403
|
+
const resolvedCommand = packageManager === "yarn"
|
|
7404
|
+
? `yarn ${scriptName}`
|
|
7405
|
+
: packageManager === "bun"
|
|
7406
|
+
? `bun run ${scriptName}`
|
|
7407
|
+
: `${packageManager} run ${scriptName}`;
|
|
7408
|
+
return {
|
|
7409
|
+
command: resolvedCommand,
|
|
7410
|
+
resolvedCommand,
|
|
7411
|
+
profile,
|
|
7412
|
+
};
|
|
7413
|
+
}
|
|
7414
|
+
async resolveGitPathspecsForRepo(repoRoot, resolvedCwd, rawPaths, workspaceContext) {
|
|
7415
|
+
const canonicalRepoRoot = await realpath(repoRoot).catch(() => repoRoot);
|
|
7416
|
+
const cleanedPaths = uniqueStrings((rawPaths || []).map((item) => asString(item)).filter(Boolean));
|
|
7417
|
+
const pathspecs = [];
|
|
7418
|
+
for (const rawPath of cleanedPaths) {
|
|
7419
|
+
const resolvedPath = workspaceContext
|
|
7420
|
+
? assertPathInsideWorkspace(workspaceContext, rawPath, { baseCwd: resolvedCwd })
|
|
7421
|
+
: expandUserPathLike(rawPath, resolvedCwd);
|
|
7422
|
+
const canonicalResolvedPath = await realpath(resolvedPath).catch(() => resolvedPath);
|
|
7423
|
+
const relativePath = path.relative(canonicalRepoRoot, canonicalResolvedPath) || ".";
|
|
7424
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
7425
|
+
throw new Error(`O caminho ${rawPath} fica fora do repositório Git ativo (${repoRoot}).`);
|
|
7426
|
+
}
|
|
7427
|
+
pathspecs.push(relativePath === "" ? "." : relativePath.split(path.sep).join("/"));
|
|
7428
|
+
}
|
|
7429
|
+
return uniqueStrings(pathspecs);
|
|
7430
|
+
}
|
|
7431
|
+
async gitCloneSnapshot(repository, destinationPath, options, workspaceContext) {
|
|
7432
|
+
const capturedAt = new Date().toISOString();
|
|
7433
|
+
const resolvedDestinationPath = workspaceContext
|
|
7434
|
+
? assertPathInsideWorkspace(workspaceContext, destinationPath)
|
|
7435
|
+
: expandUserPathLike(destinationPath);
|
|
7436
|
+
const resolvedParentCwd = workspaceContext
|
|
7437
|
+
? assertCwdInsideWorkspace(workspaceContext, path.dirname(resolvedDestinationPath))
|
|
7438
|
+
: path.dirname(resolvedDestinationPath);
|
|
7439
|
+
const cloneArgs = ["clone"];
|
|
7440
|
+
if (options.depth && options.depth > 0) {
|
|
7441
|
+
cloneArgs.push("--depth", String(options.depth));
|
|
7442
|
+
}
|
|
7443
|
+
const cloneBranch = asString(options.branch) || "";
|
|
7444
|
+
if (cloneBranch) {
|
|
7445
|
+
cloneArgs.push("--branch", cloneBranch);
|
|
7446
|
+
}
|
|
7447
|
+
cloneArgs.push(repository, resolvedDestinationPath);
|
|
7448
|
+
const cloneResult = await this.runCommandCapture("/usr/bin/git", cloneArgs, {
|
|
7449
|
+
cwd: resolvedParentCwd,
|
|
7450
|
+
allowNonZeroExit: true,
|
|
7451
|
+
});
|
|
7452
|
+
if (cloneResult.exitCode !== 0) {
|
|
7453
|
+
return {
|
|
7454
|
+
captured_at: capturedAt,
|
|
7455
|
+
repository,
|
|
7456
|
+
destination_path: destinationPath,
|
|
7457
|
+
resolved_destination_path: resolvedDestinationPath,
|
|
7458
|
+
resolved_parent_cwd: resolvedParentCwd,
|
|
7459
|
+
branch: asString(options.branch) || undefined,
|
|
7460
|
+
depth: options.depth,
|
|
7461
|
+
cloned: false,
|
|
7462
|
+
error_message: this.summarizeGitCommandError(cloneResult, "O Git recusou a operacao de clone."),
|
|
7463
|
+
summary: `O Git recusou o clone para ${path.basename(resolvedDestinationPath) || resolvedDestinationPath}.`,
|
|
7464
|
+
};
|
|
7465
|
+
}
|
|
7466
|
+
const repo = await this.probeGitRepository(resolvedDestinationPath, workspaceContext);
|
|
7467
|
+
const shaProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "HEAD"], {
|
|
7468
|
+
cwd: repo.resolvedCwd,
|
|
7469
|
+
allowNonZeroExit: true,
|
|
7470
|
+
});
|
|
7471
|
+
const commitSha = shaProbe.exitCode === 0 ? (shaProbe.stdout.trim() || undefined) : undefined;
|
|
7472
|
+
return {
|
|
7473
|
+
captured_at: capturedAt,
|
|
7474
|
+
repository,
|
|
7475
|
+
destination_path: destinationPath,
|
|
7476
|
+
resolved_destination_path: resolvedDestinationPath,
|
|
7477
|
+
resolved_parent_cwd: resolvedParentCwd,
|
|
7478
|
+
branch: asString(options.branch) || undefined,
|
|
7479
|
+
depth: options.depth,
|
|
7480
|
+
cloned: true,
|
|
7481
|
+
repo_root: repo.repoRoot || repo.resolvedCwd,
|
|
7482
|
+
current_branch: repo.currentBranch,
|
|
7483
|
+
commit_sha: commitSha,
|
|
7484
|
+
summary: `Clonei o repositorio em ${path.basename(resolvedDestinationPath) || resolvedDestinationPath}.`,
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
async gitFetchSnapshot(cwd, options, workspaceContext) {
|
|
7488
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7489
|
+
if (!repo.isRepo) {
|
|
7490
|
+
return {
|
|
7491
|
+
captured_at: repo.capturedAt,
|
|
7492
|
+
cwd,
|
|
7493
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7494
|
+
is_repo: false,
|
|
7495
|
+
remote: options.remote,
|
|
7496
|
+
prune: options.prune === true,
|
|
7497
|
+
tags: options.tags === true,
|
|
7498
|
+
fetched: false,
|
|
7499
|
+
updated_ref_count: 0,
|
|
7500
|
+
updated_refs: [],
|
|
7501
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7502
|
+
};
|
|
7503
|
+
}
|
|
7504
|
+
const fetchArgs = ["fetch"];
|
|
7505
|
+
if (options.prune === true) {
|
|
7506
|
+
fetchArgs.push("--prune");
|
|
7507
|
+
}
|
|
7508
|
+
if (options.tags === true) {
|
|
7509
|
+
fetchArgs.push("--tags");
|
|
7510
|
+
}
|
|
7511
|
+
const fetchRemote = asString(options.remote) || (repo.trackingRef ? repo.trackingRef.split("/", 1)[0] : "") || "";
|
|
7512
|
+
if (fetchRemote) {
|
|
7513
|
+
fetchArgs.push(fetchRemote, `+refs/heads/*:refs/remotes/${fetchRemote}/*`);
|
|
7514
|
+
}
|
|
7515
|
+
const fetchResult = await this.runCommandCapture("/usr/bin/git", fetchArgs, {
|
|
7516
|
+
cwd: repo.resolvedCwd,
|
|
7517
|
+
allowNonZeroExit: true,
|
|
7518
|
+
});
|
|
7519
|
+
const gitOutput = [fetchResult.stdout.trim(), fetchResult.stderr.trim()].filter(Boolean).join("\n").trim();
|
|
7520
|
+
if (fetchResult.exitCode !== 0) {
|
|
7521
|
+
return {
|
|
7522
|
+
captured_at: repo.capturedAt,
|
|
7523
|
+
cwd,
|
|
7524
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7525
|
+
is_repo: true,
|
|
7526
|
+
repo_root: repo.repoRoot,
|
|
7527
|
+
current_branch: repo.currentBranch,
|
|
7528
|
+
remote: asString(options.remote) || undefined,
|
|
7529
|
+
prune: options.prune === true,
|
|
7530
|
+
tags: options.tags === true,
|
|
7531
|
+
fetched: false,
|
|
7532
|
+
tracking_ref: repo.trackingRef,
|
|
7533
|
+
updated_ref_count: 0,
|
|
7534
|
+
updated_refs: [],
|
|
7535
|
+
error_message: clipText(gitOutput || "O Git recusou a operacao de fetch.", 4_000),
|
|
7536
|
+
summary: `O Git recusou o fetch em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
const updatedRefs = this.parseGitFetchUpdatedRefs(gitOutput);
|
|
7540
|
+
const updatedRefCount = updatedRefs.length;
|
|
7541
|
+
return {
|
|
7542
|
+
captured_at: repo.capturedAt,
|
|
7543
|
+
cwd,
|
|
7544
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7545
|
+
is_repo: true,
|
|
7546
|
+
repo_root: repo.repoRoot,
|
|
7547
|
+
current_branch: repo.currentBranch,
|
|
7548
|
+
remote: asString(options.remote) || undefined,
|
|
7549
|
+
prune: options.prune === true,
|
|
7550
|
+
tags: options.tags === true,
|
|
7551
|
+
fetched: true,
|
|
7552
|
+
tracking_ref: repo.trackingRef,
|
|
7553
|
+
updated_ref_count: updatedRefCount,
|
|
7554
|
+
updated_refs: updatedRefs,
|
|
7555
|
+
summary: updatedRefCount > 0
|
|
7556
|
+
? `Atualizei ${updatedRefCount} ref${updatedRefCount === 1 ? "" : "s"} remota${updatedRefCount === 1 ? "" : "s"} em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`
|
|
7557
|
+
: `Rodei o fetch do repositorio ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7558
|
+
};
|
|
7559
|
+
}
|
|
7560
|
+
async gitCheckoutSnapshot(cwd, options, workspaceContext) {
|
|
7561
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7562
|
+
if (!repo.isRepo) {
|
|
7563
|
+
return {
|
|
7564
|
+
captured_at: repo.capturedAt,
|
|
7565
|
+
cwd,
|
|
7566
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7567
|
+
is_repo: false,
|
|
7568
|
+
target: options.target,
|
|
7569
|
+
start_point: options.startPoint,
|
|
7570
|
+
create_branch: options.createBranch === true,
|
|
7571
|
+
detach: options.detach === true,
|
|
7572
|
+
switched: false,
|
|
7573
|
+
created_branch: false,
|
|
7574
|
+
detached_head: false,
|
|
7575
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7576
|
+
};
|
|
7577
|
+
}
|
|
7578
|
+
const checkoutArgs = ["checkout"];
|
|
7579
|
+
if (options.detach === true) {
|
|
7580
|
+
checkoutArgs.push("--detach", options.target);
|
|
7581
|
+
}
|
|
7582
|
+
else if (options.createBranch === true) {
|
|
7583
|
+
checkoutArgs.push("-b", options.target);
|
|
7584
|
+
const startPoint = asString(options.startPoint) || "";
|
|
7585
|
+
if (startPoint) {
|
|
7586
|
+
checkoutArgs.push(startPoint);
|
|
7587
|
+
}
|
|
7588
|
+
}
|
|
7589
|
+
else {
|
|
7590
|
+
checkoutArgs.push(options.target);
|
|
7591
|
+
}
|
|
7592
|
+
const checkoutResult = await this.runCommandCapture("/usr/bin/git", checkoutArgs, {
|
|
7593
|
+
cwd: repo.resolvedCwd,
|
|
7594
|
+
allowNonZeroExit: true,
|
|
7595
|
+
});
|
|
7596
|
+
if (checkoutResult.exitCode !== 0) {
|
|
7597
|
+
return {
|
|
7598
|
+
captured_at: repo.capturedAt,
|
|
7599
|
+
cwd,
|
|
7600
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7601
|
+
is_repo: true,
|
|
7602
|
+
repo_root: repo.repoRoot,
|
|
7603
|
+
current_branch: repo.currentBranch,
|
|
7604
|
+
target: options.target,
|
|
7605
|
+
start_point: options.startPoint,
|
|
7606
|
+
create_branch: options.createBranch === true,
|
|
7607
|
+
detach: options.detach === true,
|
|
7608
|
+
switched: false,
|
|
7609
|
+
created_branch: false,
|
|
7610
|
+
detached_head: false,
|
|
7611
|
+
error_message: this.summarizeGitCommandError(checkoutResult, "O Git recusou a operacao de checkout."),
|
|
7612
|
+
summary: `O Git recusou o checkout em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7613
|
+
};
|
|
7614
|
+
}
|
|
7615
|
+
const updatedRepo = await this.probeGitRepository(repo.resolvedCwd, workspaceContext);
|
|
7616
|
+
const shaProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "HEAD"], {
|
|
7617
|
+
cwd: repo.resolvedCwd,
|
|
7618
|
+
allowNonZeroExit: true,
|
|
7619
|
+
});
|
|
7620
|
+
const headSha = shaProbe.exitCode === 0 ? (shaProbe.stdout.trim() || undefined) : undefined;
|
|
7621
|
+
const detachedHead = updatedRepo.currentBranch === "HEAD" || options.detach === true;
|
|
7622
|
+
const repoLabel = path.basename(updatedRepo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd;
|
|
7623
|
+
const summary = detachedHead
|
|
7624
|
+
? `Coloquei ${repoLabel} em detached HEAD em ${options.target}.`
|
|
7625
|
+
: options.createBranch === true
|
|
7626
|
+
? `Criei e troquei para a branch ${options.target} em ${repoLabel}.`
|
|
7627
|
+
: `Troquei para ${options.target} em ${repoLabel}.`;
|
|
7628
|
+
return {
|
|
7629
|
+
captured_at: repo.capturedAt,
|
|
7630
|
+
cwd,
|
|
7631
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7632
|
+
is_repo: true,
|
|
7633
|
+
repo_root: updatedRepo.repoRoot,
|
|
7634
|
+
current_branch: updatedRepo.currentBranch,
|
|
7635
|
+
target: options.target,
|
|
7636
|
+
start_point: options.startPoint,
|
|
7637
|
+
create_branch: options.createBranch === true,
|
|
7638
|
+
detach: options.detach === true,
|
|
7639
|
+
switched: true,
|
|
7640
|
+
created_branch: options.createBranch === true,
|
|
7641
|
+
detached_head: detachedHead,
|
|
7642
|
+
head_sha: headSha,
|
|
7643
|
+
summary,
|
|
7644
|
+
};
|
|
7645
|
+
}
|
|
7646
|
+
async gitRebaseSnapshot(cwd, options, workspaceContext) {
|
|
7647
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7648
|
+
if (!repo.isRepo) {
|
|
7649
|
+
return {
|
|
7650
|
+
captured_at: repo.capturedAt,
|
|
7651
|
+
cwd,
|
|
7652
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7653
|
+
is_repo: false,
|
|
7654
|
+
target: options.target,
|
|
7655
|
+
autostash: options.autostash === true,
|
|
7656
|
+
rebased: false,
|
|
7657
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7658
|
+
};
|
|
7659
|
+
}
|
|
7660
|
+
const rebaseArgs = ["rebase"];
|
|
7661
|
+
if (options.autostash === true) {
|
|
7662
|
+
rebaseArgs.push("--autostash");
|
|
7663
|
+
}
|
|
7664
|
+
rebaseArgs.push(options.target);
|
|
7665
|
+
const rebaseResult = await this.runCommandCapture("/usr/bin/git", rebaseArgs, {
|
|
7666
|
+
cwd: repo.resolvedCwd,
|
|
7667
|
+
allowNonZeroExit: true,
|
|
7668
|
+
});
|
|
7669
|
+
if (rebaseResult.exitCode !== 0) {
|
|
7670
|
+
return {
|
|
7671
|
+
captured_at: repo.capturedAt,
|
|
7672
|
+
cwd,
|
|
7673
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7674
|
+
is_repo: true,
|
|
7675
|
+
repo_root: repo.repoRoot,
|
|
7676
|
+
current_branch: repo.currentBranch,
|
|
7677
|
+
target: options.target,
|
|
7678
|
+
autostash: options.autostash === true,
|
|
7679
|
+
rebased: false,
|
|
7680
|
+
error_message: this.summarizeGitCommandError(rebaseResult, "O Git recusou a operacao de rebase."),
|
|
7681
|
+
summary: `O Git recusou o rebase em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7682
|
+
};
|
|
7683
|
+
}
|
|
7684
|
+
const updatedRepo = await this.probeGitRepository(repo.resolvedCwd, workspaceContext);
|
|
7685
|
+
const shaProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "HEAD"], {
|
|
7686
|
+
cwd: repo.resolvedCwd,
|
|
7687
|
+
allowNonZeroExit: true,
|
|
7688
|
+
});
|
|
7689
|
+
const headSha = shaProbe.exitCode === 0 ? (shaProbe.stdout.trim() || undefined) : undefined;
|
|
7690
|
+
return {
|
|
7691
|
+
captured_at: repo.capturedAt,
|
|
7692
|
+
cwd,
|
|
7693
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7694
|
+
is_repo: true,
|
|
7695
|
+
repo_root: updatedRepo.repoRoot,
|
|
7696
|
+
current_branch: updatedRepo.currentBranch,
|
|
7697
|
+
target: options.target,
|
|
7698
|
+
autostash: options.autostash === true,
|
|
7699
|
+
rebased: true,
|
|
7700
|
+
head_sha: headSha,
|
|
7701
|
+
summary: `Rebaseei a branch atual sobre ${options.target} em ${path.basename(updatedRepo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7702
|
+
};
|
|
7703
|
+
}
|
|
7704
|
+
async gitMergeSnapshot(cwd, options, workspaceContext) {
|
|
7705
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7706
|
+
if (!repo.isRepo) {
|
|
7707
|
+
return {
|
|
7708
|
+
captured_at: repo.capturedAt,
|
|
7709
|
+
cwd,
|
|
7710
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7711
|
+
is_repo: false,
|
|
7712
|
+
target: options.target,
|
|
7713
|
+
ff_only: options.ffOnly === true,
|
|
7714
|
+
no_ff: options.noFf === true,
|
|
7715
|
+
merged: false,
|
|
7716
|
+
fast_forward: false,
|
|
7717
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7718
|
+
};
|
|
7719
|
+
}
|
|
7720
|
+
const mergeArgs = ["merge"];
|
|
7721
|
+
if (options.ffOnly === true) {
|
|
7722
|
+
mergeArgs.push("--ff-only");
|
|
7723
|
+
}
|
|
7724
|
+
else if (options.noFf === true) {
|
|
7725
|
+
mergeArgs.push("--no-ff");
|
|
7726
|
+
}
|
|
7727
|
+
const mergeMessage = asString(options.message) || "";
|
|
7728
|
+
if (mergeMessage) {
|
|
7729
|
+
mergeArgs.push("-m", mergeMessage);
|
|
7730
|
+
}
|
|
7731
|
+
mergeArgs.push(options.target);
|
|
7732
|
+
const mergeResult = await this.runCommandCapture("/usr/bin/git", mergeArgs, {
|
|
7733
|
+
cwd: repo.resolvedCwd,
|
|
7734
|
+
allowNonZeroExit: true,
|
|
7735
|
+
});
|
|
7736
|
+
const gitOutput = [mergeResult.stdout.trim(), mergeResult.stderr.trim()].filter(Boolean).join("\n").trim();
|
|
7737
|
+
if (mergeResult.exitCode !== 0) {
|
|
7738
|
+
return {
|
|
7739
|
+
captured_at: repo.capturedAt,
|
|
7740
|
+
cwd,
|
|
7741
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7742
|
+
is_repo: true,
|
|
7743
|
+
repo_root: repo.repoRoot,
|
|
7744
|
+
current_branch: repo.currentBranch,
|
|
7745
|
+
target: options.target,
|
|
7746
|
+
ff_only: options.ffOnly === true,
|
|
7747
|
+
no_ff: options.noFf === true,
|
|
7748
|
+
merged: false,
|
|
7749
|
+
fast_forward: false,
|
|
7750
|
+
error_message: clipText(gitOutput || "O Git recusou a operacao de merge.", 4_000),
|
|
7751
|
+
summary: `O Git recusou o merge em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
const shaProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "HEAD"], {
|
|
7755
|
+
cwd: repo.resolvedCwd,
|
|
7756
|
+
allowNonZeroExit: true,
|
|
7757
|
+
});
|
|
7758
|
+
const commitSha = shaProbe.exitCode === 0 ? (shaProbe.stdout.trim() || undefined) : undefined;
|
|
7759
|
+
const updatedRepo = await this.probeGitRepository(repo.resolvedCwd, workspaceContext);
|
|
7760
|
+
const fastForward = /fast-forward/i.test(gitOutput);
|
|
7761
|
+
return {
|
|
7762
|
+
captured_at: repo.capturedAt,
|
|
7763
|
+
cwd,
|
|
7764
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7765
|
+
is_repo: true,
|
|
7766
|
+
repo_root: updatedRepo.repoRoot,
|
|
7767
|
+
current_branch: updatedRepo.currentBranch,
|
|
7768
|
+
target: options.target,
|
|
7769
|
+
ff_only: options.ffOnly === true,
|
|
7770
|
+
no_ff: options.noFf === true,
|
|
7771
|
+
merged: true,
|
|
7772
|
+
fast_forward: fastForward,
|
|
7773
|
+
commit_sha: commitSha,
|
|
7774
|
+
summary: `Mesclei ${options.target} em ${path.basename(updatedRepo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7775
|
+
};
|
|
7776
|
+
}
|
|
7777
|
+
async gitTagSnapshot(cwd, options, workspaceContext) {
|
|
7778
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7779
|
+
if (!repo.isRepo) {
|
|
7780
|
+
return {
|
|
7781
|
+
captured_at: repo.capturedAt,
|
|
7782
|
+
cwd,
|
|
7783
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7784
|
+
is_repo: false,
|
|
7785
|
+
name: options.name,
|
|
7786
|
+
target: options.target,
|
|
7787
|
+
annotated: options.annotated === true,
|
|
7788
|
+
message: asString(options.message) || undefined,
|
|
7789
|
+
created: false,
|
|
7790
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7791
|
+
};
|
|
7792
|
+
}
|
|
7793
|
+
const annotated = options.annotated === true || Boolean(asString(options.message));
|
|
7794
|
+
const tagArgs = ["tag"];
|
|
7795
|
+
if (annotated) {
|
|
7796
|
+
tagArgs.push("-a", options.name);
|
|
7797
|
+
const annotation = asString(options.message) || `tag: ${options.name}`;
|
|
7798
|
+
tagArgs.push("-m", annotation);
|
|
7799
|
+
}
|
|
7800
|
+
else {
|
|
7801
|
+
tagArgs.push(options.name);
|
|
7802
|
+
}
|
|
7803
|
+
const tagTarget = asString(options.target) || "";
|
|
7804
|
+
if (tagTarget) {
|
|
7805
|
+
tagArgs.push(tagTarget);
|
|
7806
|
+
}
|
|
7807
|
+
const tagResult = await this.runCommandCapture("/usr/bin/git", tagArgs, {
|
|
7808
|
+
cwd: repo.resolvedCwd,
|
|
7809
|
+
allowNonZeroExit: true,
|
|
7810
|
+
});
|
|
7811
|
+
if (tagResult.exitCode !== 0) {
|
|
7812
|
+
return {
|
|
7813
|
+
captured_at: repo.capturedAt,
|
|
7814
|
+
cwd,
|
|
7815
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7816
|
+
is_repo: true,
|
|
7817
|
+
repo_root: repo.repoRoot,
|
|
7818
|
+
current_branch: repo.currentBranch,
|
|
7819
|
+
name: options.name,
|
|
7820
|
+
target: options.target,
|
|
7821
|
+
annotated,
|
|
7822
|
+
message: asString(options.message) || undefined,
|
|
7823
|
+
created: false,
|
|
7824
|
+
error_message: this.summarizeGitCommandError(tagResult, "O Git recusou a criacao da tag."),
|
|
7825
|
+
summary: `O Git recusou a criacao da tag ${options.name} em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7826
|
+
};
|
|
7827
|
+
}
|
|
7828
|
+
const tagProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", options.name], {
|
|
7829
|
+
cwd: repo.resolvedCwd,
|
|
7830
|
+
allowNonZeroExit: true,
|
|
7831
|
+
});
|
|
7832
|
+
const tagRef = tagProbe.exitCode === 0 ? (tagProbe.stdout.trim() || undefined) : undefined;
|
|
7833
|
+
return {
|
|
7834
|
+
captured_at: repo.capturedAt,
|
|
7835
|
+
cwd,
|
|
7836
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7837
|
+
is_repo: true,
|
|
7838
|
+
repo_root: repo.repoRoot,
|
|
7839
|
+
current_branch: repo.currentBranch,
|
|
7840
|
+
name: options.name,
|
|
7841
|
+
target: options.target,
|
|
7842
|
+
annotated,
|
|
7843
|
+
message: asString(options.message) || undefined,
|
|
7844
|
+
created: true,
|
|
7845
|
+
tag_ref: tagRef,
|
|
7846
|
+
summary: `Criei a tag ${options.name} em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
parseGitStatusBranchLine(branchLine) {
|
|
7850
|
+
const line = branchLine.replace(/^##\s*/, "").trim();
|
|
7851
|
+
if (!line) {
|
|
7852
|
+
return { ahead: 0, behind: 0 };
|
|
7853
|
+
}
|
|
7854
|
+
const relationMatch = line.match(/^([^.\s]+)(?:\.\.\.([^\s]+))?(?:\s+\[(.+)\])?$/);
|
|
7855
|
+
const currentBranch = relationMatch?.[1]?.trim() || (line.startsWith("HEAD") ? "HEAD" : undefined);
|
|
7856
|
+
const upstreamBranch = relationMatch?.[2]?.trim() || undefined;
|
|
7857
|
+
const relation = relationMatch?.[3] || "";
|
|
7858
|
+
const aheadMatch = relation.match(/ahead\s+(\d+)/i);
|
|
7859
|
+
const behindMatch = relation.match(/behind\s+(\d+)/i);
|
|
7860
|
+
return {
|
|
7861
|
+
currentBranch,
|
|
7862
|
+
upstreamBranch,
|
|
7863
|
+
ahead: aheadMatch ? Number(aheadMatch[1]) : 0,
|
|
7864
|
+
behind: behindMatch ? Number(behindMatch[1]) : 0,
|
|
7865
|
+
};
|
|
7866
|
+
}
|
|
7867
|
+
parseGitStatusEntries(lines) {
|
|
7868
|
+
return lines
|
|
7869
|
+
.map((line) => line.trimEnd())
|
|
7870
|
+
.filter(Boolean)
|
|
7871
|
+
.map((line) => {
|
|
7872
|
+
const stagedStatus = line.slice(0, 1) || " ";
|
|
7873
|
+
const unstagedStatus = line.slice(1, 2) || " ";
|
|
7874
|
+
const payload = line.slice(3).trim();
|
|
7875
|
+
if (!payload) {
|
|
7876
|
+
return null;
|
|
7877
|
+
}
|
|
7878
|
+
const renameMatch = payload.match(/^(.*?)\s+->\s+(.*)$/);
|
|
7879
|
+
return {
|
|
7880
|
+
path: (renameMatch?.[2] || payload).trim(),
|
|
7881
|
+
staged_status: stagedStatus,
|
|
7882
|
+
unstaged_status: unstagedStatus,
|
|
7883
|
+
original_path: renameMatch?.[1]?.trim() || undefined,
|
|
7884
|
+
};
|
|
7885
|
+
})
|
|
7886
|
+
.filter((item) => item !== null);
|
|
7887
|
+
}
|
|
7888
|
+
async gitAddSnapshot(cwd, options, workspaceContext) {
|
|
7889
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7890
|
+
const stagedAll = options.all === true || !(options.paths && options.paths.length > 0);
|
|
7891
|
+
const requestedPaths = uniqueStrings((options.paths || []).map((item) => asString(item)).filter(Boolean));
|
|
7892
|
+
if (!repo.isRepo) {
|
|
7893
|
+
return {
|
|
7894
|
+
captured_at: repo.capturedAt,
|
|
7895
|
+
cwd,
|
|
7896
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7897
|
+
is_repo: false,
|
|
7898
|
+
staged_all: stagedAll,
|
|
7899
|
+
path_count: requestedPaths.length,
|
|
7900
|
+
staged_count: 0,
|
|
7901
|
+
staged_paths: [],
|
|
7902
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7903
|
+
};
|
|
7904
|
+
}
|
|
7905
|
+
const pathspecs = stagedAll
|
|
7906
|
+
? []
|
|
7907
|
+
: await this.resolveGitPathspecsForRepo(repo.repoRoot || repo.resolvedCwd, repo.resolvedCwd, requestedPaths, workspaceContext);
|
|
7908
|
+
const addArgs = ["add"];
|
|
7909
|
+
if (stagedAll || pathspecs.length === 0) {
|
|
7910
|
+
addArgs.push("--all");
|
|
7911
|
+
}
|
|
7912
|
+
if (pathspecs.length > 0) {
|
|
7913
|
+
addArgs.push("--", ...pathspecs);
|
|
7914
|
+
}
|
|
7915
|
+
const addResult = await this.runCommandCapture("/usr/bin/git", addArgs, {
|
|
7916
|
+
cwd: repo.resolvedCwd,
|
|
7917
|
+
allowNonZeroExit: true,
|
|
7918
|
+
});
|
|
7919
|
+
if (addResult.exitCode !== 0) {
|
|
7920
|
+
return {
|
|
7921
|
+
captured_at: repo.capturedAt,
|
|
7922
|
+
cwd,
|
|
7923
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7924
|
+
is_repo: true,
|
|
7925
|
+
repo_root: repo.repoRoot,
|
|
7926
|
+
current_branch: repo.currentBranch,
|
|
7927
|
+
staged_all: stagedAll,
|
|
7928
|
+
path_count: pathspecs.length,
|
|
7929
|
+
staged_count: 0,
|
|
7930
|
+
staged_paths: [],
|
|
7931
|
+
error_message: this.summarizeGitCommandError(addResult, "O Git recusou a operacao de stage."),
|
|
7932
|
+
summary: `O Git recusou a preparacao do stage em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
7933
|
+
};
|
|
7934
|
+
}
|
|
7935
|
+
const nameOnlyArgs = ["diff", "--cached", "--name-only"];
|
|
7936
|
+
if (pathspecs.length > 0) {
|
|
7937
|
+
nameOnlyArgs.push("--", ...pathspecs);
|
|
7938
|
+
}
|
|
7939
|
+
const stagedResult = await this.runCommandCapture("/usr/bin/git", nameOnlyArgs, {
|
|
7940
|
+
cwd: repo.resolvedCwd,
|
|
7941
|
+
allowNonZeroExit: true,
|
|
7942
|
+
});
|
|
7943
|
+
const stagedPaths = uniqueStrings(stagedResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
7944
|
+
const stagedCount = stagedPaths.length;
|
|
7945
|
+
const summary = stagedCount === 0
|
|
7946
|
+
? stagedAll
|
|
7947
|
+
? `Nao havia mudancas novas para stage em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`
|
|
7948
|
+
: `Nenhum dos caminhos pedidos gerou stage novo em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`
|
|
7949
|
+
: stagedAll
|
|
7950
|
+
? `Preparei o stage das mudancas atuais em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`
|
|
7951
|
+
: `Preparei o stage de ${stagedCount} caminho${stagedCount === 1 ? "" : "s"} em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`;
|
|
7952
|
+
return {
|
|
7953
|
+
captured_at: repo.capturedAt,
|
|
7954
|
+
cwd,
|
|
7955
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7956
|
+
is_repo: true,
|
|
7957
|
+
repo_root: repo.repoRoot,
|
|
7958
|
+
current_branch: repo.currentBranch,
|
|
7959
|
+
staged_all: stagedAll,
|
|
7960
|
+
path_count: pathspecs.length,
|
|
7961
|
+
staged_count: stagedCount,
|
|
7962
|
+
staged_paths: stagedPaths,
|
|
7963
|
+
summary,
|
|
7964
|
+
};
|
|
7965
|
+
}
|
|
7966
|
+
async gitCommitSnapshot(cwd, message, allowEmpty, workspaceContext) {
|
|
7967
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7968
|
+
if (!repo.isRepo) {
|
|
7969
|
+
return {
|
|
7970
|
+
captured_at: repo.capturedAt,
|
|
7971
|
+
cwd,
|
|
7972
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7973
|
+
is_repo: false,
|
|
7974
|
+
message,
|
|
7975
|
+
allow_empty: allowEmpty,
|
|
7976
|
+
committed: false,
|
|
7977
|
+
nothing_to_commit: false,
|
|
7978
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
7979
|
+
};
|
|
7980
|
+
}
|
|
7981
|
+
const commitArgs = allowEmpty
|
|
7982
|
+
? ["commit", "--allow-empty", "-m", message]
|
|
7983
|
+
: ["commit", "-m", message];
|
|
7984
|
+
const commitResult = await this.runCommandCapture("/usr/bin/git", commitArgs, {
|
|
7985
|
+
cwd: repo.resolvedCwd,
|
|
7986
|
+
allowNonZeroExit: true,
|
|
7987
|
+
});
|
|
7988
|
+
if (commitResult.exitCode !== 0) {
|
|
7989
|
+
const gitError = this.summarizeGitCommandError(commitResult, "O Git recusou a operacao de commit.");
|
|
7990
|
+
const nothingToCommit = /nothing to commit|no changes added to commit|working tree clean/i.test(gitError);
|
|
7991
|
+
return {
|
|
7992
|
+
captured_at: repo.capturedAt,
|
|
7993
|
+
cwd,
|
|
7994
|
+
resolved_cwd: repo.resolvedCwd,
|
|
7995
|
+
is_repo: true,
|
|
7996
|
+
repo_root: repo.repoRoot,
|
|
7997
|
+
current_branch: repo.currentBranch,
|
|
7998
|
+
message,
|
|
7999
|
+
allow_empty: allowEmpty,
|
|
8000
|
+
committed: false,
|
|
8001
|
+
nothing_to_commit: nothingToCommit,
|
|
8002
|
+
error_message: nothingToCommit ? undefined : gitError,
|
|
8003
|
+
summary: nothingToCommit
|
|
8004
|
+
? `Nao havia mudancas staged para criar um commit em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`
|
|
8005
|
+
: `O Git recusou a criacao do commit em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
8006
|
+
};
|
|
8007
|
+
}
|
|
8008
|
+
const shaProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "HEAD"], {
|
|
8009
|
+
cwd: repo.resolvedCwd,
|
|
8010
|
+
allowNonZeroExit: true,
|
|
8011
|
+
});
|
|
8012
|
+
const commitSha = shaProbe.exitCode === 0 ? (shaProbe.stdout.trim() || undefined) : undefined;
|
|
8013
|
+
return {
|
|
8014
|
+
captured_at: repo.capturedAt,
|
|
8015
|
+
cwd,
|
|
8016
|
+
resolved_cwd: repo.resolvedCwd,
|
|
8017
|
+
is_repo: true,
|
|
8018
|
+
repo_root: repo.repoRoot,
|
|
8019
|
+
current_branch: repo.currentBranch,
|
|
8020
|
+
message,
|
|
8021
|
+
allow_empty: allowEmpty,
|
|
8022
|
+
committed: true,
|
|
8023
|
+
nothing_to_commit: false,
|
|
8024
|
+
commit_sha: commitSha,
|
|
8025
|
+
summary: `Criei um commit local${commitSha ? ` (${commitSha.slice(0, 12)})` : ""} em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
8026
|
+
};
|
|
8027
|
+
}
|
|
8028
|
+
async gitPushSnapshot(cwd, options, workspaceContext) {
|
|
8029
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
8030
|
+
if (!repo.isRepo) {
|
|
8031
|
+
return {
|
|
8032
|
+
captured_at: repo.capturedAt,
|
|
8033
|
+
cwd,
|
|
8034
|
+
resolved_cwd: repo.resolvedCwd,
|
|
8035
|
+
is_repo: false,
|
|
8036
|
+
remote: options.remote,
|
|
8037
|
+
branch: options.branch,
|
|
8038
|
+
set_upstream: options.setUpstream === true,
|
|
8039
|
+
pushed: false,
|
|
8040
|
+
up_to_date: false,
|
|
8041
|
+
summary: `${repo.resolvedCwd} nao parece ser um repositorio Git.`,
|
|
8042
|
+
};
|
|
8043
|
+
}
|
|
8044
|
+
const branch = asString(options.branch) || repo.currentBranch || undefined;
|
|
8045
|
+
const configuredRemote = asString(options.remote) || "";
|
|
8046
|
+
const effectiveRemote = configuredRemote || (repo.trackingRef ? repo.trackingRef.split("/", 1)[0] : "");
|
|
8047
|
+
const pushArgs = ["push"];
|
|
8048
|
+
if (options.setUpstream === true) {
|
|
8049
|
+
pushArgs.push("--set-upstream");
|
|
8050
|
+
}
|
|
8051
|
+
if (effectiveRemote) {
|
|
8052
|
+
pushArgs.push(effectiveRemote);
|
|
8053
|
+
}
|
|
8054
|
+
if (branch && effectiveRemote) {
|
|
8055
|
+
pushArgs.push(branch);
|
|
8056
|
+
}
|
|
8057
|
+
const pushResult = await this.runCommandCapture("/usr/bin/git", pushArgs, {
|
|
8058
|
+
cwd: repo.resolvedCwd,
|
|
8059
|
+
allowNonZeroExit: true,
|
|
8060
|
+
});
|
|
8061
|
+
const gitOutput = [pushResult.stdout.trim(), pushResult.stderr.trim()].filter(Boolean).join("\n").trim();
|
|
8062
|
+
if (pushResult.exitCode !== 0) {
|
|
8063
|
+
return {
|
|
8064
|
+
captured_at: repo.capturedAt,
|
|
8065
|
+
cwd,
|
|
8066
|
+
resolved_cwd: repo.resolvedCwd,
|
|
8067
|
+
is_repo: true,
|
|
8068
|
+
repo_root: repo.repoRoot,
|
|
8069
|
+
current_branch: repo.currentBranch,
|
|
8070
|
+
remote: effectiveRemote || undefined,
|
|
8071
|
+
branch,
|
|
8072
|
+
set_upstream: options.setUpstream === true,
|
|
8073
|
+
pushed: false,
|
|
8074
|
+
up_to_date: false,
|
|
8075
|
+
tracking_ref: repo.trackingRef,
|
|
8076
|
+
error_message: clipText(gitOutput || "O Git recusou a operacao de push.", 4_000),
|
|
8077
|
+
summary: `O Git recusou o push em ${path.basename(repo.repoRoot || repo.resolvedCwd) || repo.resolvedCwd}.`,
|
|
8078
|
+
};
|
|
8079
|
+
}
|
|
8080
|
+
const trackingProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
8081
|
+
cwd: repo.resolvedCwd,
|
|
8082
|
+
allowNonZeroExit: true,
|
|
8083
|
+
});
|
|
8084
|
+
const trackingRef = trackingProbe.exitCode === 0
|
|
8085
|
+
? (trackingProbe.stdout.trim() || undefined)
|
|
8086
|
+
: (repo.trackingRef || (effectiveRemote && branch ? `${effectiveRemote}/${branch}` : undefined));
|
|
8087
|
+
const upToDate = /everything up-to-date/i.test(gitOutput);
|
|
8088
|
+
const remoteLabel = effectiveRemote || (trackingRef ? trackingRef.split("/", 1)[0] : "");
|
|
8089
|
+
const branchLabel = branch || repo.currentBranch || "branch atual";
|
|
8090
|
+
return {
|
|
8091
|
+
captured_at: repo.capturedAt,
|
|
8092
|
+
cwd,
|
|
8093
|
+
resolved_cwd: repo.resolvedCwd,
|
|
8094
|
+
is_repo: true,
|
|
8095
|
+
repo_root: repo.repoRoot,
|
|
8096
|
+
current_branch: repo.currentBranch,
|
|
8097
|
+
remote: remoteLabel || undefined,
|
|
8098
|
+
branch: branch || undefined,
|
|
8099
|
+
set_upstream: options.setUpstream === true,
|
|
8100
|
+
pushed: !upToDate,
|
|
8101
|
+
up_to_date: upToDate,
|
|
8102
|
+
tracking_ref: trackingRef,
|
|
8103
|
+
summary: upToDate
|
|
8104
|
+
? `A branch ${branchLabel} ja estava sincronizada${remoteLabel ? ` com ${remoteLabel}` : ""}.`
|
|
8105
|
+
: `Enviei a branch ${branchLabel}${remoteLabel ? ` para ${remoteLabel}` : ""}.`,
|
|
8106
|
+
};
|
|
8107
|
+
}
|
|
8108
|
+
async gitStatusSnapshot(cwd, includeUntracked, workspaceContext) {
|
|
8109
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd, workspaceContext);
|
|
8110
|
+
const capturedAt = new Date().toISOString();
|
|
8111
|
+
const repoProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--show-toplevel"], {
|
|
8112
|
+
cwd: resolvedCwd,
|
|
8113
|
+
allowNonZeroExit: true,
|
|
8114
|
+
});
|
|
8115
|
+
if (repoProbe.exitCode !== 0) {
|
|
8116
|
+
return {
|
|
8117
|
+
captured_at: capturedAt,
|
|
8118
|
+
cwd,
|
|
8119
|
+
resolved_cwd: resolvedCwd,
|
|
8120
|
+
is_repo: false,
|
|
8121
|
+
ahead: 0,
|
|
8122
|
+
behind: 0,
|
|
8123
|
+
include_untracked: includeUntracked,
|
|
8124
|
+
is_clean: true,
|
|
8125
|
+
entry_count: 0,
|
|
8126
|
+
staged_count: 0,
|
|
8127
|
+
modified_count: 0,
|
|
8128
|
+
deleted_count: 0,
|
|
8129
|
+
renamed_count: 0,
|
|
8130
|
+
untracked_count: 0,
|
|
8131
|
+
entries: [],
|
|
8132
|
+
summary: `${resolvedCwd} nao parece ser um repositorio Git.`,
|
|
8133
|
+
};
|
|
8134
|
+
}
|
|
8135
|
+
const repoRoot = repoProbe.stdout.trim() || resolvedCwd;
|
|
8136
|
+
const statusResult = await this.runCommandCapture("/usr/bin/git", [
|
|
8137
|
+
"status",
|
|
8138
|
+
"--short",
|
|
8139
|
+
"--branch",
|
|
8140
|
+
"--porcelain=v1",
|
|
8141
|
+
includeUntracked ? "--untracked-files=all" : "--untracked-files=no",
|
|
8142
|
+
], {
|
|
8143
|
+
cwd: resolvedCwd,
|
|
8144
|
+
allowNonZeroExit: true,
|
|
8145
|
+
});
|
|
8146
|
+
const lines = statusResult.stdout.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
8147
|
+
const branchLine = lines[0]?.startsWith("##") ? lines[0] : "";
|
|
8148
|
+
const entries = this.parseGitStatusEntries(branchLine ? lines.slice(1) : lines);
|
|
8149
|
+
const branchInfo = this.parseGitStatusBranchLine(branchLine);
|
|
8150
|
+
const stagedCount = entries.filter((entry) => ![" ", "?"].includes(entry.staged_status)).length;
|
|
8151
|
+
const modifiedCount = entries.filter((entry) => entry.staged_status === "M" || entry.unstaged_status === "M").length;
|
|
8152
|
+
const deletedCount = entries.filter((entry) => entry.staged_status === "D" || entry.unstaged_status === "D").length;
|
|
8153
|
+
const renamedCount = entries.filter((entry) => entry.staged_status === "R" || entry.unstaged_status === "R").length;
|
|
8154
|
+
const untrackedCount = entries.filter((entry) => entry.staged_status === "?" && entry.unstaged_status === "?").length;
|
|
8155
|
+
const isClean = entries.length === 0;
|
|
8156
|
+
const currentBranch = branchInfo.currentBranch;
|
|
8157
|
+
const summary = isClean
|
|
8158
|
+
? `O repositorio ${path.basename(repoRoot) || repoRoot} esta limpo${currentBranch ? ` na branch ${currentBranch}` : ""}.`
|
|
8159
|
+
: `O repositorio ${path.basename(repoRoot) || repoRoot}${currentBranch ? ` na branch ${currentBranch}` : ""} tem ${entries.length} arquivo${entries.length === 1 ? "" : "s"} alterado${entries.length === 1 ? "" : "s"} (${stagedCount} staged, ${modifiedCount} modificados, ${untrackedCount} untracked).`;
|
|
8160
|
+
return {
|
|
8161
|
+
captured_at: capturedAt,
|
|
8162
|
+
cwd,
|
|
8163
|
+
resolved_cwd: resolvedCwd,
|
|
8164
|
+
is_repo: true,
|
|
8165
|
+
repo_root: repoRoot,
|
|
8166
|
+
current_branch: currentBranch,
|
|
8167
|
+
upstream_branch: branchInfo.upstreamBranch,
|
|
8168
|
+
ahead: branchInfo.ahead,
|
|
8169
|
+
behind: branchInfo.behind,
|
|
8170
|
+
include_untracked: includeUntracked,
|
|
8171
|
+
is_clean: isClean,
|
|
8172
|
+
entry_count: entries.length,
|
|
8173
|
+
staged_count: stagedCount,
|
|
8174
|
+
modified_count: modifiedCount,
|
|
8175
|
+
deleted_count: deletedCount,
|
|
8176
|
+
renamed_count: renamedCount,
|
|
8177
|
+
untracked_count: untrackedCount,
|
|
8178
|
+
entries,
|
|
8179
|
+
summary,
|
|
8180
|
+
};
|
|
8181
|
+
}
|
|
8182
|
+
async gitDiffSnapshot(cwd, options, workspaceContext) {
|
|
8183
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd, workspaceContext);
|
|
8184
|
+
const capturedAt = new Date().toISOString();
|
|
8185
|
+
const repoProbe = await this.runCommandCapture("/usr/bin/git", ["rev-parse", "--show-toplevel"], {
|
|
8186
|
+
cwd: resolvedCwd,
|
|
8187
|
+
allowNonZeroExit: true,
|
|
8188
|
+
});
|
|
8189
|
+
const staged = options.staged === true;
|
|
8190
|
+
const baseRef = asString(options.baseRef) || undefined;
|
|
8191
|
+
if (repoProbe.exitCode !== 0) {
|
|
8192
|
+
return {
|
|
8193
|
+
captured_at: capturedAt,
|
|
8194
|
+
cwd,
|
|
8195
|
+
resolved_cwd: resolvedCwd,
|
|
8196
|
+
is_repo: false,
|
|
8197
|
+
staged,
|
|
8198
|
+
base_ref: baseRef,
|
|
8199
|
+
has_diff: false,
|
|
8200
|
+
file_count: 0,
|
|
8201
|
+
changed_files: [],
|
|
8202
|
+
stat: "",
|
|
8203
|
+
diff: "",
|
|
8204
|
+
diff_char_count: 0,
|
|
8205
|
+
truncated: false,
|
|
8206
|
+
summary: `${resolvedCwd} nao parece ser um repositorio Git.`,
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
const repoRoot = repoProbe.stdout.trim() || resolvedCwd;
|
|
8210
|
+
const diffArgs = ["diff", "--no-color"];
|
|
8211
|
+
if (staged) {
|
|
8212
|
+
diffArgs.push("--cached");
|
|
8213
|
+
}
|
|
8214
|
+
if (baseRef) {
|
|
8215
|
+
diffArgs.push(baseRef);
|
|
8216
|
+
}
|
|
8217
|
+
const scopedPaths = (options.paths || [])
|
|
8218
|
+
.map((item) => asString(item))
|
|
8219
|
+
.filter((item) => Boolean(item));
|
|
8220
|
+
const pathArgs = scopedPaths.length > 0 ? ["--", ...scopedPaths] : [];
|
|
8221
|
+
const statResult = await this.runCommandCapture("/usr/bin/git", [...diffArgs, "--stat", ...pathArgs], {
|
|
8222
|
+
cwd: resolvedCwd,
|
|
8223
|
+
allowNonZeroExit: true,
|
|
8224
|
+
});
|
|
8225
|
+
const nameOnlyResult = await this.runCommandCapture("/usr/bin/git", [...diffArgs, "--name-only", ...pathArgs], {
|
|
8226
|
+
cwd: resolvedCwd,
|
|
8227
|
+
allowNonZeroExit: true,
|
|
8228
|
+
});
|
|
8229
|
+
const rawDiffResult = await this.runCommandCapture("/usr/bin/git", [...diffArgs, "--unified=3", ...pathArgs], {
|
|
8230
|
+
cwd: resolvedCwd,
|
|
8231
|
+
allowNonZeroExit: true,
|
|
8232
|
+
});
|
|
8233
|
+
const changedFiles = uniqueStrings(nameOnlyResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
8234
|
+
const rawDiff = rawDiffResult.stdout.trim();
|
|
8235
|
+
const maxChars = Math.max(1_000, Math.min(options.maxChars || 12_000, 20_000));
|
|
8236
|
+
const clippedDiff = clipText(rawDiff || "(sem diff textual)", maxChars);
|
|
8237
|
+
const summary = changedFiles.length === 0
|
|
8238
|
+
? `Nao encontrei diff ${staged ? "staged " : ""}pendente em ${path.basename(repoRoot) || repoRoot}.`
|
|
8239
|
+
: `Encontrei diff ${staged ? "staged " : ""}com ${changedFiles.length} arquivo${changedFiles.length === 1 ? "" : "s"} alterado${changedFiles.length === 1 ? "" : "s"} em ${path.basename(repoRoot) || repoRoot}.`;
|
|
8240
|
+
return {
|
|
8241
|
+
captured_at: capturedAt,
|
|
8242
|
+
cwd,
|
|
8243
|
+
resolved_cwd: resolvedCwd,
|
|
8244
|
+
is_repo: true,
|
|
8245
|
+
repo_root: repoRoot,
|
|
8246
|
+
staged,
|
|
8247
|
+
base_ref: baseRef,
|
|
8248
|
+
has_diff: changedFiles.length > 0,
|
|
8249
|
+
file_count: changedFiles.length,
|
|
8250
|
+
changed_files: changedFiles,
|
|
8251
|
+
stat: statResult.stdout.trim(),
|
|
8252
|
+
diff: clippedDiff,
|
|
8253
|
+
diff_char_count: rawDiff.length,
|
|
8254
|
+
truncated: clippedDiff.length < rawDiff.length,
|
|
8255
|
+
summary,
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
async runTestsSnapshot(command, cwd, timeoutSeconds = 900, workspaceContext, profile, runtimeManifest) {
|
|
8259
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd, workspaceContext);
|
|
8260
|
+
const capturedAt = new Date().toISOString();
|
|
8261
|
+
const timeoutMs = Math.max(5_000, Math.min(timeoutSeconds * 1000, 1_800_000));
|
|
8262
|
+
const ladderStages = !command && (profile === "auto" || !profile)
|
|
8263
|
+
? this.resolveValidationLadderStages(runtimeManifest, resolvedCwd, workspaceContext)
|
|
8264
|
+
: [];
|
|
8265
|
+
const stagesToRun = ladderStages.length > 0
|
|
8266
|
+
? ladderStages
|
|
8267
|
+
: [{ stage_id: profile || "explicit_command", title: undefined, profile }];
|
|
8268
|
+
const stageSnapshots = [];
|
|
8269
|
+
for (const stage of stagesToRun) {
|
|
8270
|
+
const startedAt = Date.now();
|
|
8271
|
+
const resolvedTestCommand = await this.resolveRunTestsCommand(command, stage.profile, resolvedCwd, workspaceContext);
|
|
8272
|
+
const result = await this.runCommandCapture("/bin/zsh", ["-lc", resolvedTestCommand.resolvedCommand], {
|
|
8273
|
+
cwd: resolvedCwd,
|
|
8274
|
+
allowNonZeroExit: true,
|
|
8275
|
+
timeoutMs,
|
|
8276
|
+
});
|
|
8277
|
+
const combined = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
8278
|
+
const output = clipText(combined || "(sem saida)", 16_000);
|
|
8279
|
+
const passed = result.exitCode === 0 && result.timedOut !== true;
|
|
8280
|
+
const durationMs = Math.max(1, Date.now() - startedAt);
|
|
8281
|
+
const summary = result.timedOut
|
|
8282
|
+
? `A etapa ${stage.stage_id} em ${path.basename(resolvedCwd) || resolvedCwd} excedeu o limite de tempo.`
|
|
8283
|
+
: passed
|
|
8284
|
+
? `A etapa ${stage.stage_id} em ${path.basename(resolvedCwd) || resolvedCwd} passou com sucesso.`
|
|
8285
|
+
: `A etapa ${stage.stage_id} em ${path.basename(resolvedCwd) || resolvedCwd} falhou com exit code ${result.exitCode}.`;
|
|
8286
|
+
stageSnapshots.push({
|
|
8287
|
+
stage_id: stage.stage_id,
|
|
8288
|
+
title: stage.title,
|
|
8289
|
+
profile: resolvedTestCommand.profile,
|
|
8290
|
+
command: resolvedTestCommand.command,
|
|
8291
|
+
resolved_command: resolvedTestCommand.resolvedCommand,
|
|
8292
|
+
passed,
|
|
8293
|
+
exit_code: result.exitCode,
|
|
8294
|
+
timed_out: result.timedOut,
|
|
8295
|
+
duration_ms: durationMs,
|
|
8296
|
+
output,
|
|
8297
|
+
output_char_count: combined.length,
|
|
8298
|
+
summary,
|
|
8299
|
+
});
|
|
8300
|
+
if (!passed) {
|
|
8301
|
+
break;
|
|
8302
|
+
}
|
|
8303
|
+
if (command) {
|
|
8304
|
+
break;
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8307
|
+
const firstStage = stageSnapshots[0];
|
|
8308
|
+
const failedStage = stageSnapshots.find((stage) => !stage.passed);
|
|
8309
|
+
const overallPassed = stageSnapshots.length > 0 && failedStage === undefined;
|
|
8310
|
+
const combinedOutput = clipText(stageSnapshots
|
|
8311
|
+
.map((stage) => `# ${stage.stage_id}\n${stage.output}`)
|
|
8312
|
+
.join("\n\n")
|
|
8313
|
+
|| "(sem saida)", 16_000);
|
|
8314
|
+
const totalDurationMs = stageSnapshots.reduce((total, stage) => total + stage.duration_ms, 0);
|
|
8315
|
+
const resolvedCommandLabel = stageSnapshots.map((stage) => stage.resolved_command).join(" && ");
|
|
8316
|
+
const summary = failedStage
|
|
8317
|
+
? failedStage.summary
|
|
8318
|
+
: `As ${stageSnapshots.length === 1 ? "validacao" : "validacoes"} em ${path.basename(resolvedCwd) || resolvedCwd} passaram com sucesso.`;
|
|
8319
|
+
return {
|
|
8320
|
+
captured_at: capturedAt,
|
|
8321
|
+
cwd,
|
|
8322
|
+
resolved_cwd: resolvedCwd,
|
|
8323
|
+
command: command || (firstStage?.command || resolvedCommandLabel || "validation_ladder"),
|
|
8324
|
+
profile: profile || firstStage?.profile,
|
|
8325
|
+
resolved_command: resolvedCommandLabel || firstStage?.resolved_command || "",
|
|
8326
|
+
passed: overallPassed,
|
|
8327
|
+
exit_code: failedStage?.exit_code ?? firstStage?.exit_code ?? 0,
|
|
8328
|
+
timed_out: failedStage?.timed_out ?? firstStage?.timed_out ?? false,
|
|
8329
|
+
duration_ms: totalDurationMs || firstStage?.duration_ms || 1,
|
|
8330
|
+
output: combinedOutput,
|
|
8331
|
+
output_char_count: combinedOutput.length,
|
|
8332
|
+
summary,
|
|
8333
|
+
selected_ladder_id: ladderStages.length > 0 ? runtimeManifest?.validationLadder?.ladder_id : undefined,
|
|
8334
|
+
stage_count: stageSnapshots.length,
|
|
8335
|
+
failed_stage_id: failedStage?.stage_id,
|
|
8336
|
+
stages: stageSnapshots,
|
|
8337
|
+
};
|
|
8338
|
+
}
|
|
8339
|
+
async runShellCommand(command, cwd, workspaceContext) {
|
|
5765
8340
|
if (!isSafeShellCommand(command)) {
|
|
5766
8341
|
throw new Error("Nenhum comando shell foi informado para execucao local.");
|
|
5767
8342
|
}
|
|
5768
|
-
const resolvedCwd = cwd
|
|
8343
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd || "", workspaceContext);
|
|
5769
8344
|
const { stdout, stderr } = await this.runCommandCapture("/bin/zsh", ["-lc", command], {
|
|
5770
8345
|
cwd: resolvedCwd,
|
|
5771
8346
|
});
|
|
@@ -5832,15 +8407,66 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5832
8407
|
if (action.type === "write_text_file") {
|
|
5833
8408
|
return `Arquivo de texto escrito em ${action.filename ? `${action.path}/${action.filename}` : action.path}`;
|
|
5834
8409
|
}
|
|
8410
|
+
if (action.type === "write_json_file") {
|
|
8411
|
+
return `Arquivo JSON escrito em ${action.filename ? `${action.path}/${action.filename}` : action.path}`;
|
|
8412
|
+
}
|
|
8413
|
+
if (action.type === "apply_patch") {
|
|
8414
|
+
return `Patch aplicado em ${action.cwd}`;
|
|
8415
|
+
}
|
|
8416
|
+
if (action.type === "mkdir") {
|
|
8417
|
+
return `Pasta criada em ${action.path}`;
|
|
8418
|
+
}
|
|
8419
|
+
if (action.type === "move_file") {
|
|
8420
|
+
return `Item movido de ${action.source_path} para ${action.destination_path}`;
|
|
8421
|
+
}
|
|
8422
|
+
if (action.type === "git_clone") {
|
|
8423
|
+
return `Repositorio Git clonado em ${action.destination_path}`;
|
|
8424
|
+
}
|
|
8425
|
+
if (action.type === "git_fetch") {
|
|
8426
|
+
return `Fetch do Git executado em ${action.cwd}`;
|
|
8427
|
+
}
|
|
8428
|
+
if (action.type === "git_checkout") {
|
|
8429
|
+
return `Checkout do Git executado em ${action.cwd}`;
|
|
8430
|
+
}
|
|
8431
|
+
if (action.type === "git_rebase") {
|
|
8432
|
+
return `Rebase do Git executado em ${action.cwd}`;
|
|
8433
|
+
}
|
|
8434
|
+
if (action.type === "git_merge") {
|
|
8435
|
+
return `Merge do Git executado em ${action.cwd}`;
|
|
8436
|
+
}
|
|
8437
|
+
if (action.type === "git_tag") {
|
|
8438
|
+
return `Tag ${action.name} criada em ${action.cwd}`;
|
|
8439
|
+
}
|
|
8440
|
+
if (action.type === "git_add") {
|
|
8441
|
+
return `Stage do Git preparado em ${action.cwd}`;
|
|
8442
|
+
}
|
|
8443
|
+
if (action.type === "git_commit") {
|
|
8444
|
+
return `Commit local criado em ${action.cwd}`;
|
|
8445
|
+
}
|
|
8446
|
+
if (action.type === "git_push") {
|
|
8447
|
+
return `Push do Git executado em ${action.cwd}`;
|
|
8448
|
+
}
|
|
5835
8449
|
if (action.type === "list_files") {
|
|
5836
8450
|
return `Arquivos listados em ${action.path}`;
|
|
5837
8451
|
}
|
|
8452
|
+
if (action.type === "delete_file") {
|
|
8453
|
+
return `Arquivo apagado em ${action.path}`;
|
|
8454
|
+
}
|
|
5838
8455
|
if (action.type === "count_files") {
|
|
5839
8456
|
return `Arquivos contados em ${action.path}`;
|
|
5840
8457
|
}
|
|
5841
8458
|
if (action.type === "system_status") {
|
|
5842
8459
|
return "Status do macOS coletado";
|
|
5843
8460
|
}
|
|
8461
|
+
if (action.type === "git_status") {
|
|
8462
|
+
return `Estado do Git lido em ${action.cwd}`;
|
|
8463
|
+
}
|
|
8464
|
+
if (action.type === "git_diff") {
|
|
8465
|
+
return `Diff do Git lido em ${action.cwd}`;
|
|
8466
|
+
}
|
|
8467
|
+
if (action.type === "run_tests") {
|
|
8468
|
+
return `Validacao ${action.profile || "local"} executada em ${action.cwd}`;
|
|
8469
|
+
}
|
|
5844
8470
|
if (action.type === "run_shell") {
|
|
5845
8471
|
return `Comando ${action.command} executado no macOS`;
|
|
5846
8472
|
}
|
|
@@ -5875,9 +8501,31 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5875
8501
|
});
|
|
5876
8502
|
this.activeChild = child;
|
|
5877
8503
|
try {
|
|
5878
|
-
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
8504
|
+
const { stdout, stderr, exitCode, timedOut } = await new Promise((resolve, reject) => {
|
|
5879
8505
|
let stdout = "";
|
|
5880
8506
|
let stderr = "";
|
|
8507
|
+
let timedOut = false;
|
|
8508
|
+
let settled = false;
|
|
8509
|
+
const timeoutMs = options?.timeoutMs;
|
|
8510
|
+
const timer = timeoutMs && timeoutMs > 0
|
|
8511
|
+
? setTimeout(() => {
|
|
8512
|
+
timedOut = true;
|
|
8513
|
+
child.kill("SIGTERM");
|
|
8514
|
+
setTimeout(() => {
|
|
8515
|
+
try {
|
|
8516
|
+
child.kill("SIGKILL");
|
|
8517
|
+
}
|
|
8518
|
+
catch {
|
|
8519
|
+
// Process already exited.
|
|
8520
|
+
}
|
|
8521
|
+
}, 1000).unref();
|
|
8522
|
+
}, timeoutMs)
|
|
8523
|
+
: null;
|
|
8524
|
+
const clearTimer = () => {
|
|
8525
|
+
if (timer) {
|
|
8526
|
+
clearTimeout(timer);
|
|
8527
|
+
}
|
|
8528
|
+
};
|
|
5881
8529
|
if (options?.stdin !== undefined) {
|
|
5882
8530
|
child.stdin.write(options.stdin);
|
|
5883
8531
|
child.stdin.end();
|
|
@@ -5892,21 +8540,36 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5892
8540
|
stderr += String(chunk);
|
|
5893
8541
|
});
|
|
5894
8542
|
child.on("error", (error) => {
|
|
8543
|
+
if (settled) {
|
|
8544
|
+
return;
|
|
8545
|
+
}
|
|
8546
|
+
settled = true;
|
|
8547
|
+
clearTimer();
|
|
5895
8548
|
reject(error);
|
|
5896
8549
|
});
|
|
5897
8550
|
child.on("close", (code) => {
|
|
5898
|
-
if (
|
|
5899
|
-
|
|
8551
|
+
if (settled) {
|
|
8552
|
+
return;
|
|
8553
|
+
}
|
|
8554
|
+
settled = true;
|
|
8555
|
+
clearTimer();
|
|
8556
|
+
const normalizedExitCode = typeof code === "number" ? code : (timedOut ? 124 : 1);
|
|
8557
|
+
if (timedOut && options?.allowNonZeroExit !== true) {
|
|
8558
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`));
|
|
8559
|
+
return;
|
|
8560
|
+
}
|
|
8561
|
+
if (normalizedExitCode === 0 || options?.allowNonZeroExit === true) {
|
|
8562
|
+
resolve({ stdout, stderr, exitCode: normalizedExitCode, timedOut });
|
|
5900
8563
|
return;
|
|
5901
8564
|
}
|
|
5902
|
-
reject(new Error(`${command} ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit code ${
|
|
8565
|
+
reject(new Error(`${command} ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit code ${normalizedExitCode}`}`));
|
|
5903
8566
|
});
|
|
5904
8567
|
});
|
|
5905
8568
|
const stderrText = stderr.trim();
|
|
5906
8569
|
if (stderrText) {
|
|
5907
8570
|
console.warn(`[otto-bridge] ${command} stderr=${stderrText}`);
|
|
5908
8571
|
}
|
|
5909
|
-
return { stdout, stderr };
|
|
8572
|
+
return { stdout, stderr, exitCode, timedOut };
|
|
5910
8573
|
}
|
|
5911
8574
|
catch (error) {
|
|
5912
8575
|
const detail = error instanceof Error ? error.message : String(error);
|