@leg3ndy/otto-bridge 0.9.1 → 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 +3341 -381
- 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 +164 -0
- 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, stat } 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) {
|
|
@@ -702,6 +702,17 @@ function clipTextPreview(value, maxLength) {
|
|
|
702
702
|
}
|
|
703
703
|
return `${value.slice(0, maxLength)}\n\n[conteudo truncado: mostrando ${maxLength} de ${value.length} caracteres. Peca um trecho mais especifico se quiser continuar.]`;
|
|
704
704
|
}
|
|
705
|
+
function sanitizeFileName(value, fallback = "otto-note.txt") {
|
|
706
|
+
const normalized = String(value || "")
|
|
707
|
+
.normalize("NFD")
|
|
708
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
709
|
+
.replace(/[\/\\?%*:|"<>]/g, "-")
|
|
710
|
+
.replace(/\s+/g, " ")
|
|
711
|
+
.trim();
|
|
712
|
+
const collapsed = normalized.replace(/\.+/g, ".").replace(/^\.+/, "").replace(/\.+$/, "");
|
|
713
|
+
const candidate = collapsed || fallback;
|
|
714
|
+
return path.extname(candidate) ? candidate : `${candidate}.txt`;
|
|
715
|
+
}
|
|
705
716
|
function chunkTextForTransport(value, chunkSize) {
|
|
706
717
|
const normalized = String(value || "");
|
|
707
718
|
if (!normalized) {
|
|
@@ -1009,6 +1020,269 @@ function parseStructuredActions(job) {
|
|
|
1009
1020
|
}
|
|
1010
1021
|
continue;
|
|
1011
1022
|
}
|
|
1023
|
+
if (type === "write_text_file" || type === "write_file" || type === "save_text_file" || type === "save_file") {
|
|
1024
|
+
const targetPath = asString(action.path)
|
|
1025
|
+
|| asString(action.destination)
|
|
1026
|
+
|| asString(action.file_path)
|
|
1027
|
+
|| asString(action.target)
|
|
1028
|
+
|| asString(action.directory)
|
|
1029
|
+
|| asString(action.folder)
|
|
1030
|
+
|| (asString(action.filename) ? "~/Desktop" : "");
|
|
1031
|
+
const text = asString(action.text) || asString(action.content) || asString(action.body) || asString(action.value);
|
|
1032
|
+
const source = asString(action.source) || asString(action.input_source);
|
|
1033
|
+
if (targetPath && (text || source)) {
|
|
1034
|
+
actions.push({
|
|
1035
|
+
type: "write_text_file",
|
|
1036
|
+
path: targetPath,
|
|
1037
|
+
text: text || undefined,
|
|
1038
|
+
content: asString(action.content) || undefined,
|
|
1039
|
+
body: asString(action.body) || undefined,
|
|
1040
|
+
source: source || undefined,
|
|
1041
|
+
filename: asString(action.filename) || asString(action.file_name) || asString(action.name) || asString(action.title) || undefined,
|
|
1042
|
+
append: action.append === true,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
continue;
|
|
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
|
+
}
|
|
1012
1286
|
if (type === "list_files" || type === "ls") {
|
|
1013
1287
|
const filePath = asString(action.path) || "~";
|
|
1014
1288
|
const limit = typeof action.limit === "number" ? Math.max(1, Math.min(5_000, action.limit)) : undefined;
|
|
@@ -1045,6 +1319,13 @@ function parseStructuredActions(job) {
|
|
|
1045
1319
|
}
|
|
1046
1320
|
continue;
|
|
1047
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
|
+
}
|
|
1048
1329
|
if (type === "set_volume" || type === "volume") {
|
|
1049
1330
|
const rawLevel = Number(action.level);
|
|
1050
1331
|
if (Number.isFinite(rawLevel)) {
|
|
@@ -1214,6 +1495,46 @@ function extractActions(job) {
|
|
|
1214
1495
|
}
|
|
1215
1496
|
return deriveActionsFromText(job);
|
|
1216
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
|
+
}
|
|
1217
1538
|
export class NativeMacOSJobExecutor {
|
|
1218
1539
|
bridgeConfig;
|
|
1219
1540
|
cancelledJobs = new Set();
|
|
@@ -1221,6 +1542,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1221
1542
|
lastActiveApp = null;
|
|
1222
1543
|
lastVisualTargetDescription = null;
|
|
1223
1544
|
lastVisualTargetApp = null;
|
|
1545
|
+
lastReadFrontmostPage = null;
|
|
1224
1546
|
lastSatisfiedSpotifyDescription = null;
|
|
1225
1547
|
lastSatisfiedSpotifyConfirmedPlaying = false;
|
|
1226
1548
|
lastSatisfiedSpotifyAt = 0;
|
|
@@ -1237,6 +1559,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1237
1559
|
throw new Error("The native-macos executor only runs on macOS");
|
|
1238
1560
|
}
|
|
1239
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
|
+
});
|
|
1240
1570
|
if (actions.length === 0) {
|
|
1241
1571
|
throw new Error("Otto Bridge native-macos could not derive a supported local action from this request");
|
|
1242
1572
|
}
|
|
@@ -1249,39 +1579,307 @@ export class NativeMacOSJobExecutor {
|
|
|
1249
1579
|
const decision = await reporter.confirmRequired(confirmation.message, {
|
|
1250
1580
|
actions,
|
|
1251
1581
|
executor: "native-macos",
|
|
1582
|
+
}, {
|
|
1583
|
+
stepId: runtimeManifest.approvalStepId,
|
|
1252
1584
|
});
|
|
1253
1585
|
if (decision.action !== "approve") {
|
|
1254
1586
|
throw new JobCancelledError(job.job_id);
|
|
1255
1587
|
}
|
|
1256
1588
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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,
|
|
1262
1769
|
actions,
|
|
1263
1770
|
artifacts,
|
|
1264
|
-
|
|
1265
|
-
|
|
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 {
|
|
1266
1805
|
for (let index = 0; index < actions.length; index += 1) {
|
|
1267
1806
|
this.assertNotCancelled(job.job_id);
|
|
1268
1807
|
const action = actions[index];
|
|
1269
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
|
+
};
|
|
1270
1828
|
if (action.type === "open_app") {
|
|
1271
|
-
await
|
|
1829
|
+
await reportActionProgress(`Abrindo ${action.app} no macOS`);
|
|
1272
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
|
+
});
|
|
1273
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
|
+
});
|
|
1274
1846
|
continue;
|
|
1275
1847
|
}
|
|
1276
1848
|
if (action.type === "focus_app") {
|
|
1277
|
-
await
|
|
1849
|
+
await reportActionProgress(`Trazendo ${action.app} para frente`);
|
|
1278
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
|
+
});
|
|
1279
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
|
+
});
|
|
1280
1866
|
continue;
|
|
1281
1867
|
}
|
|
1282
1868
|
if (action.type === "press_shortcut") {
|
|
1283
|
-
await
|
|
1869
|
+
await reportActionProgress(`Enviando atalho ${action.shortcut}`);
|
|
1284
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
|
+
});
|
|
1285
1883
|
if (action.shortcut.startsWith("media_")) {
|
|
1286
1884
|
const mediaSummaryMap = {
|
|
1287
1885
|
media_next: "Acionei o comando de próxima mídia no macOS.",
|
|
@@ -1303,14 +1901,23 @@ export class NativeMacOSJobExecutor {
|
|
|
1303
1901
|
continue;
|
|
1304
1902
|
}
|
|
1305
1903
|
if (action.type === "create_note") {
|
|
1306
|
-
await
|
|
1904
|
+
await reportActionProgress("Criando nota no Notes");
|
|
1307
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
|
+
});
|
|
1308
1915
|
completionNotes.push(`Nota criada no Notes: ${noteTitle}`);
|
|
1309
1916
|
continue;
|
|
1310
1917
|
}
|
|
1311
1918
|
if (action.type === "type_text") {
|
|
1312
1919
|
const typingApp = this.lastActiveApp || await this.getFrontmostAppName();
|
|
1313
|
-
await
|
|
1920
|
+
await reportActionProgress(`Digitando texto em ${typingApp || "app ativo"}`);
|
|
1314
1921
|
const typed = await this.guidedTypeText(action.text, typingApp || undefined);
|
|
1315
1922
|
if (!typed.ok) {
|
|
1316
1923
|
throw new Error(typed.reason || "Nao consegui digitar o texto no app ativo.");
|
|
@@ -1322,6 +1929,15 @@ export class NativeMacOSJobExecutor {
|
|
|
1322
1929
|
attempts: typed.attempts,
|
|
1323
1930
|
text_preview: clipText(action.text, 180),
|
|
1324
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
|
+
});
|
|
1325
1941
|
this.lastVisualTargetDescription = null;
|
|
1326
1942
|
this.lastVisualTargetApp = null;
|
|
1327
1943
|
completionNotes.push(typed.verified
|
|
@@ -1330,14 +1946,16 @@ export class NativeMacOSJobExecutor {
|
|
|
1330
1946
|
continue;
|
|
1331
1947
|
}
|
|
1332
1948
|
if (action.type === "take_screenshot") {
|
|
1333
|
-
await
|
|
1949
|
+
await reportActionProgress("Capturando screenshot do Mac");
|
|
1334
1950
|
const screenshotPath = await this.takeScreenshot(action.path);
|
|
1335
1951
|
const uploadable = await this.buildUploadableImage(screenshotPath);
|
|
1952
|
+
const expectedArtifactId = runtimeExpectedArtifactIdForActionIndex(runtimeManifest, index, "screenshot");
|
|
1336
1953
|
const screenshotArtifact = await this.uploadArtifactForJob(job.job_id, uploadable.path, {
|
|
1337
1954
|
kind: "screenshot",
|
|
1338
1955
|
mimeTypeOverride: uploadable.mimeType,
|
|
1339
1956
|
fileNameOverride: uploadable.filename,
|
|
1340
1957
|
metadata: {
|
|
1958
|
+
expected_artifact_id: expectedArtifactId || undefined,
|
|
1341
1959
|
visible_in_chat: true,
|
|
1342
1960
|
width: uploadable.dimensions?.width || undefined,
|
|
1343
1961
|
height: uploadable.dimensions?.height || undefined,
|
|
@@ -1357,10 +1975,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1357
1975
|
continue;
|
|
1358
1976
|
}
|
|
1359
1977
|
if (action.type === "read_frontmost_page") {
|
|
1360
|
-
await
|
|
1978
|
+
await reportActionProgress(`Lendo a pagina ativa em ${action.app || "Safari"}`);
|
|
1361
1979
|
const page = await this.readFrontmostPage(action.app || "Safari");
|
|
1980
|
+
this.lastReadFrontmostPage = {
|
|
1981
|
+
app: action.app || "Safari",
|
|
1982
|
+
...page,
|
|
1983
|
+
};
|
|
1362
1984
|
if (!page.text && this.bridgeConfig?.apiBaseUrl && this.bridgeConfig?.deviceToken) {
|
|
1363
|
-
await
|
|
1985
|
+
await reportActionProgress("Safari bloqueou leitura direta; vou analisar a pagina pela tela");
|
|
1364
1986
|
const screenshotPath = await this.takeScreenshot();
|
|
1365
1987
|
const uploadable = await this.buildUploadableImage(screenshotPath);
|
|
1366
1988
|
const artifact = await this.uploadArtifactForJob(job.job_id, uploadable.path, {
|
|
@@ -1389,52 +2011,424 @@ export class NativeMacOSJobExecutor {
|
|
|
1389
2011
|
}
|
|
1390
2012
|
}
|
|
1391
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
|
+
});
|
|
1392
2020
|
completionNotes.push(`Li a pagina ${page.title || page.url || "ativa"} no navegador.`);
|
|
1393
2021
|
continue;
|
|
1394
2022
|
}
|
|
1395
2023
|
if (action.type === "browser_context") {
|
|
1396
|
-
await
|
|
2024
|
+
await reportActionProgress("Lendo o contexto do navegador ativo");
|
|
1397
2025
|
const browserContext = await this.collectBrowserContext(action.app, action.include_text === true, action.include_tabs === true);
|
|
1398
2026
|
resultPayload.browser_context = browserContext;
|
|
1399
2027
|
resultPayload.summary = browserContext.summary;
|
|
2028
|
+
appendActionArtifact("browser_context", {
|
|
2029
|
+
summary: browserContext.summary,
|
|
2030
|
+
title: browserContext.title || undefined,
|
|
2031
|
+
path: browserContext.url || undefined,
|
|
2032
|
+
});
|
|
1400
2033
|
completionNotes.push(browserContext.summary);
|
|
1401
2034
|
continue;
|
|
1402
2035
|
}
|
|
1403
2036
|
if (action.type === "app_status") {
|
|
1404
|
-
await
|
|
2037
|
+
await reportActionProgress("Lendo os apps ativos do Mac");
|
|
1405
2038
|
const appStatus = await this.collectAppStatus(action.app, action.include_frontmost === true, action.include_running_apps === true, action.include_top_processes === true);
|
|
1406
2039
|
resultPayload.app_status = appStatus;
|
|
1407
2040
|
resultPayload.summary = appStatus.summary;
|
|
2041
|
+
appendActionArtifact("app_status", {
|
|
2042
|
+
summary: appStatus.summary,
|
|
2043
|
+
app: action.app || appStatus.frontmost_app || undefined,
|
|
2044
|
+
});
|
|
1408
2045
|
completionNotes.push(appStatus.summary);
|
|
1409
2046
|
continue;
|
|
1410
2047
|
}
|
|
1411
2048
|
if (action.type === "filesystem_inspect") {
|
|
1412
|
-
await
|
|
1413
|
-
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);
|
|
1414
2051
|
resultPayload.filesystem = filesystem;
|
|
1415
2052
|
resultPayload.summary = filesystem.summary;
|
|
2053
|
+
appendActionArtifact("filesystem_snapshot", {
|
|
2054
|
+
summary: filesystem.summary,
|
|
2055
|
+
path: filesystem.resolved_path,
|
|
2056
|
+
});
|
|
1416
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
|
+
});
|
|
1417
2063
|
continue;
|
|
1418
2064
|
}
|
|
1419
2065
|
if (action.type === "read_file") {
|
|
1420
|
-
await
|
|
1421
|
-
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);
|
|
1422
2068
|
resultPayload.read_file = fileContent;
|
|
1423
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
|
+
});
|
|
1424
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
|
+
});
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (action.type === "trash_path") {
|
|
2085
|
+
await reportActionProgress(`Movendo ${action.path} para a Lixeira`);
|
|
2086
|
+
const trashed = await this.movePathToTrashSnapshot(action.path, workspaceContext);
|
|
2087
|
+
resultPayload.trash_path = trashed;
|
|
2088
|
+
resultPayload.summary = trashed.summary;
|
|
2089
|
+
appendActionArtifact("trash_receipt", {
|
|
2090
|
+
summary: trashed.summary,
|
|
2091
|
+
path: trashed.trashed_path,
|
|
2092
|
+
filename: trashed.name,
|
|
2093
|
+
});
|
|
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
|
+
});
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (action.type === "write_text_file") {
|
|
2121
|
+
const targetLabel = action.filename ? `${action.path}/${action.filename}` : action.path;
|
|
2122
|
+
await reportActionProgress(`Escrevendo arquivo de texto em ${targetLabel}`);
|
|
2123
|
+
const resolvedContent = this.resolveWriteTextFileContent(action);
|
|
2124
|
+
if (!resolvedContent) {
|
|
2125
|
+
throw new Error("Nenhum texto foi informado para gravar no arquivo local.");
|
|
2126
|
+
}
|
|
2127
|
+
const written = await this.writeTextFileSnapshot(action.path, resolvedContent.content, action.filename, action.append === true, resolvedContent.source, workspaceContext);
|
|
2128
|
+
resultPayload.write_text_file = written;
|
|
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
|
+
});
|
|
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
|
+
});
|
|
1425
2410
|
continue;
|
|
1426
2411
|
}
|
|
1427
2412
|
if (action.type === "list_files") {
|
|
1428
|
-
await
|
|
1429
|
-
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);
|
|
1430
2415
|
resultPayload.file_listing = listing;
|
|
1431
2416
|
resultPayload.summary = listing.summary;
|
|
2417
|
+
appendActionArtifact("file_listing", {
|
|
2418
|
+
summary: listing.summary,
|
|
2419
|
+
path: listing.resolved_path,
|
|
2420
|
+
});
|
|
1432
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
|
+
});
|
|
1433
2427
|
continue;
|
|
1434
2428
|
}
|
|
1435
2429
|
if (action.type === "count_files") {
|
|
1436
|
-
await
|
|
1437
|
-
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);
|
|
1438
2432
|
completionNotes.push(`Encontrei ${counted.total} arquivo${counted.total === 1 ? "" : "s"} ${counted.extensionsLabel} em ${counted.path}.`);
|
|
1439
2433
|
resultPayload.file_count = {
|
|
1440
2434
|
total: counted.total,
|
|
@@ -1442,36 +2436,145 @@ export class NativeMacOSJobExecutor {
|
|
|
1442
2436
|
extensions: counted.extensions,
|
|
1443
2437
|
recursive: counted.recursive,
|
|
1444
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
|
+
});
|
|
1445
2448
|
continue;
|
|
1446
2449
|
}
|
|
1447
2450
|
if (action.type === "system_status") {
|
|
1448
|
-
await
|
|
2451
|
+
await reportActionProgress("Lendo CPU, memoria, disco e bateria do Mac");
|
|
1449
2452
|
const systemStatus = await this.collectSystemStatus(action.sections, action.include_top_processes === true);
|
|
1450
2453
|
resultPayload.system_status = systemStatus;
|
|
1451
2454
|
resultPayload.summary = systemStatus.summary;
|
|
2455
|
+
appendActionArtifact("system_status", {
|
|
2456
|
+
summary: systemStatus.summary,
|
|
2457
|
+
});
|
|
1452
2458
|
completionNotes.push(systemStatus.summary);
|
|
1453
2459
|
continue;
|
|
1454
2460
|
}
|
|
1455
|
-
if (action.type === "
|
|
1456
|
-
await
|
|
1457
|
-
const
|
|
1458
|
-
|
|
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
|
+
});
|
|
1459
2477
|
continue;
|
|
1460
2478
|
}
|
|
1461
|
-
if (action.type === "
|
|
1462
|
-
await
|
|
1463
|
-
await this.
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
+
});
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
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;
|
|
1466
2569
|
}
|
|
1467
2570
|
if (action.type === "scroll_view") {
|
|
1468
2571
|
const scrollApp = action.app || this.lastActiveApp || await this.getFrontmostAppName();
|
|
1469
2572
|
if (scrollApp) {
|
|
1470
|
-
await
|
|
2573
|
+
await reportActionProgress(`Trazendo ${scrollApp} para frente antes de rolar a tela`);
|
|
1471
2574
|
await this.focusApp(scrollApp);
|
|
1472
2575
|
}
|
|
1473
2576
|
const directionLabel = action.direction === "up" ? "cima" : "baixo";
|
|
1474
|
-
await
|
|
2577
|
+
await reportActionProgress(`Rolando a tela para ${directionLabel}`);
|
|
1475
2578
|
await this.scrollView(action.direction, action.amount, action.steps);
|
|
1476
2579
|
resultPayload.last_scroll = {
|
|
1477
2580
|
direction: action.direction,
|
|
@@ -1479,11 +2582,15 @@ export class NativeMacOSJobExecutor {
|
|
|
1479
2582
|
steps: action.steps || 1,
|
|
1480
2583
|
app: scrollApp || null,
|
|
1481
2584
|
};
|
|
2585
|
+
appendActionArtifact("viewport_change", {
|
|
2586
|
+
summary: `Rolei a tela para ${directionLabel} no macOS.`,
|
|
2587
|
+
app: scrollApp || null,
|
|
2588
|
+
});
|
|
1482
2589
|
completionNotes.push(`Rolei a tela para ${directionLabel} no macOS.`);
|
|
1483
2590
|
continue;
|
|
1484
2591
|
}
|
|
1485
2592
|
if (action.type === "whatsapp_send_message") {
|
|
1486
|
-
await
|
|
2593
|
+
await reportActionProgress(`Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1487
2594
|
await this.ensureWhatsAppWebReady();
|
|
1488
2595
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1489
2596
|
if (!selected) {
|
|
@@ -1493,7 +2600,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1493
2600
|
messages: [],
|
|
1494
2601
|
summary: "",
|
|
1495
2602
|
}));
|
|
1496
|
-
await
|
|
2603
|
+
await reportActionProgress(`Enviando a mensagem para ${action.contact} no WhatsApp`);
|
|
1497
2604
|
await this.sendWhatsAppMessage(action.text);
|
|
1498
2605
|
await delay(900);
|
|
1499
2606
|
const afterSend = await this.readWhatsAppVisibleConversation(action.contact, Math.max(12, beforeSend.messages.length + 4)).catch(() => null);
|
|
@@ -1504,17 +2611,22 @@ export class NativeMacOSJobExecutor {
|
|
|
1504
2611
|
messages: afterSend?.messages || [],
|
|
1505
2612
|
summary: afterSend?.summary || "",
|
|
1506
2613
|
};
|
|
2614
|
+
appendActionArtifact("message_delivery", {
|
|
2615
|
+
summary: `Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`,
|
|
2616
|
+
contact: action.contact,
|
|
2617
|
+
});
|
|
1507
2618
|
const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
|
|
1508
2619
|
if (!verification.ok) {
|
|
1509
2620
|
resultPayload.summary = verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`;
|
|
1510
|
-
|
|
2621
|
+
attachWorkspaceMemory("failed");
|
|
2622
|
+
await stepReporter.failed(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`, resultPayload);
|
|
1511
2623
|
return;
|
|
1512
2624
|
}
|
|
1513
2625
|
completionNotes.push(`Enviei no WhatsApp para ${action.contact}: ${clipText(action.text, 180)}`);
|
|
1514
2626
|
continue;
|
|
1515
2627
|
}
|
|
1516
2628
|
if (action.type === "whatsapp_read_chat") {
|
|
1517
|
-
await
|
|
2629
|
+
await reportActionProgress(`Abrindo a conversa do WhatsApp com ${action.contact}`);
|
|
1518
2630
|
await this.ensureWhatsAppWebReady();
|
|
1519
2631
|
const selected = await this.selectWhatsAppConversation(action.contact);
|
|
1520
2632
|
if (!selected) {
|
|
@@ -1527,17 +2639,21 @@ export class NativeMacOSJobExecutor {
|
|
|
1527
2639
|
contact: action.contact,
|
|
1528
2640
|
messages: chat.messages,
|
|
1529
2641
|
};
|
|
2642
|
+
appendActionArtifact("message_snapshot", {
|
|
2643
|
+
summary: `Mensagens visiveis no WhatsApp com ${action.contact}.`,
|
|
2644
|
+
contact: action.contact,
|
|
2645
|
+
});
|
|
1530
2646
|
completionNotes.push(`Mensagens visiveis no WhatsApp com ${action.contact}:\n${chat.summary}`);
|
|
1531
2647
|
continue;
|
|
1532
2648
|
}
|
|
1533
2649
|
if (action.type === "click_visual_target") {
|
|
1534
2650
|
const browserApp = await this.resolveLikelyBrowserApp(action.app);
|
|
1535
2651
|
if (browserApp) {
|
|
1536
|
-
await
|
|
2652
|
+
await reportActionProgress(`Trazendo ${browserApp} para frente antes do clique`);
|
|
1537
2653
|
await this.focusApp(browserApp);
|
|
1538
2654
|
}
|
|
1539
2655
|
else if (action.app) {
|
|
1540
|
-
await
|
|
2656
|
+
await reportActionProgress(`Trazendo ${action.app} para frente antes do clique`);
|
|
1541
2657
|
await this.focusApp(action.app);
|
|
1542
2658
|
}
|
|
1543
2659
|
const targetDescriptions = isSpotifySafariDomOnlyStep(action.description)
|
|
@@ -1553,7 +2669,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1553
2669
|
const isSpotifySafariStep = browserApp === "Safari" && isSpotifySafariDomOnlyStep(targetDescription);
|
|
1554
2670
|
const verificationPrompt = isSpotifySafariStep ? undefined : action.verification_prompt;
|
|
1555
2671
|
if (isSpotifySafariStep) {
|
|
1556
|
-
await
|
|
2672
|
+
await reportActionProgress(`Tentando concluir ${targetDescription} pelo DOM do Spotify no Safari`);
|
|
1557
2673
|
const spotifyDomResult = await this.executeSpotifySafariDomStep(targetDescription, initialBrowserState);
|
|
1558
2674
|
if (spotifyDomResult.ok) {
|
|
1559
2675
|
this.rememberSatisfiedSpotifyStep(targetDescription, !!spotifyDomResult.confirmedPlaying);
|
|
@@ -1579,7 +2695,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1579
2695
|
}
|
|
1580
2696
|
const nativeMediaTransport = extractNativeMediaTransportCommand(targetDescription);
|
|
1581
2697
|
if (nativeMediaTransport) {
|
|
1582
|
-
await
|
|
2698
|
+
await reportActionProgress(`Tentando controle de mídia nativo do macOS para ${targetDescription}`);
|
|
1583
2699
|
try {
|
|
1584
2700
|
await this.triggerMacOSMediaTransport(nativeMediaTransport);
|
|
1585
2701
|
let validated = false;
|
|
@@ -1590,7 +2706,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1590
2706
|
validationReason = browserValidation.reason;
|
|
1591
2707
|
}
|
|
1592
2708
|
if (!validated && verificationPrompt) {
|
|
1593
|
-
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");
|
|
1594
2710
|
if (verification.unavailable) {
|
|
1595
2711
|
lastFailureReason = verification.reason;
|
|
1596
2712
|
break;
|
|
@@ -1611,14 +2727,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1611
2727
|
break;
|
|
1612
2728
|
}
|
|
1613
2729
|
lastFailureReason = validationReason || `O controle de mídia nativo do macOS nao confirmou ${targetDescription}.`;
|
|
1614
|
-
await
|
|
2730
|
+
await reportActionProgress("O controle de mídia nativo nao foi suficiente; vou tentar DOM/OCR");
|
|
1615
2731
|
}
|
|
1616
2732
|
catch (error) {
|
|
1617
2733
|
lastFailureReason = error instanceof Error ? error.message : String(error);
|
|
1618
2734
|
}
|
|
1619
2735
|
}
|
|
1620
2736
|
if (browserApp === "Safari") {
|
|
1621
|
-
await
|
|
2737
|
+
await reportActionProgress(`Tentando localizar ${targetDescription} diretamente no Safari`);
|
|
1622
2738
|
const domClick = await this.trySafariDomClick(targetDescription);
|
|
1623
2739
|
if (domClick?.clicked) {
|
|
1624
2740
|
let validated = false;
|
|
@@ -1629,7 +2745,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1629
2745
|
validationReason = browserValidation.reason;
|
|
1630
2746
|
}
|
|
1631
2747
|
if (!validated && verificationPrompt) {
|
|
1632
|
-
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");
|
|
1633
2749
|
if (verification.unavailable) {
|
|
1634
2750
|
lastFailureReason = verification.reason;
|
|
1635
2751
|
break;
|
|
@@ -1663,7 +2779,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1663
2779
|
const visualBeforeState = browserApp
|
|
1664
2780
|
? await this.captureBrowserPageState(browserApp).catch(() => initialBrowserState)
|
|
1665
2781
|
: initialBrowserState;
|
|
1666
|
-
await
|
|
2782
|
+
await reportActionProgress(`Capturando a tela para localizar ${targetDescription}`);
|
|
1667
2783
|
let screenshotPath = await this.takeScreenshot();
|
|
1668
2784
|
const ocrClick = await this.tryLocalOcrClick(screenshotPath, targetDescription);
|
|
1669
2785
|
if (ocrClick.clicked) {
|
|
@@ -1675,7 +2791,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1675
2791
|
validationReason = browserValidation.reason;
|
|
1676
2792
|
}
|
|
1677
2793
|
if (!validated && verificationPrompt) {
|
|
1678
|
-
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");
|
|
1679
2795
|
if (verification.unavailable) {
|
|
1680
2796
|
lastFailureReason = verification.reason;
|
|
1681
2797
|
break;
|
|
@@ -1704,7 +2820,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1704
2820
|
break;
|
|
1705
2821
|
}
|
|
1706
2822
|
lastFailureReason = validationReason || `O clique por OCR local em ${targetDescription} nao teve efeito confirmavel.`;
|
|
1707
|
-
await
|
|
2823
|
+
await reportActionProgress("OCR local nao confirmou o clique; vou tentar visão remota");
|
|
1708
2824
|
screenshotPath = await this.takeScreenshot();
|
|
1709
2825
|
}
|
|
1710
2826
|
else if (ocrClick.reason) {
|
|
@@ -1745,7 +2861,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1745
2861
|
}
|
|
1746
2862
|
continue;
|
|
1747
2863
|
}
|
|
1748
|
-
await
|
|
2864
|
+
await reportActionProgress(`Clicando em ${targetDescription}`);
|
|
1749
2865
|
const scaledX = width > 0 && originalWidth > 0 ? (location.x / width) * originalWidth : location.x;
|
|
1750
2866
|
const scaledY = height > 0 && originalHeight > 0 ? (location.y / height) * originalHeight : location.y;
|
|
1751
2867
|
await this.clickPoint(scaledX, scaledY);
|
|
@@ -1756,7 +2872,7 @@ export class NativeMacOSJobExecutor {
|
|
|
1756
2872
|
strategy: "visual_locator",
|
|
1757
2873
|
};
|
|
1758
2874
|
if (verificationPrompt) {
|
|
1759
|
-
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");
|
|
1760
2876
|
if (verification.unavailable) {
|
|
1761
2877
|
lastFailureReason = verification.reason;
|
|
1762
2878
|
break;
|
|
@@ -1782,19 +2898,22 @@ export class NativeMacOSJobExecutor {
|
|
|
1782
2898
|
if (!clickSucceeded) {
|
|
1783
2899
|
throw new Error(lastFailureReason || `Nao consegui concluir o clique visual para ${action.description}.`);
|
|
1784
2900
|
}
|
|
2901
|
+
appendActionArtifact("interaction_result", {
|
|
2902
|
+
summary: `Localizei e cliquei em ${action.description}.`,
|
|
2903
|
+
});
|
|
1785
2904
|
continue;
|
|
1786
2905
|
}
|
|
1787
2906
|
if (action.type === "drag_visual_target") {
|
|
1788
2907
|
const dragApp = await this.resolveLikelyBrowserApp(action.app);
|
|
1789
2908
|
if (dragApp) {
|
|
1790
|
-
await
|
|
2909
|
+
await reportActionProgress(`Trazendo ${dragApp} para frente antes do arraste`);
|
|
1791
2910
|
await this.focusApp(dragApp);
|
|
1792
2911
|
}
|
|
1793
2912
|
else if (action.app) {
|
|
1794
|
-
await
|
|
2913
|
+
await reportActionProgress(`Trazendo ${action.app} para frente antes do arraste`);
|
|
1795
2914
|
await this.focusApp(action.app);
|
|
1796
2915
|
}
|
|
1797
|
-
await
|
|
2916
|
+
await reportActionProgress(`Capturando a tela para localizar ${action.source_description} e ${action.target_description}`);
|
|
1798
2917
|
const screenshotPath = await this.takeScreenshot();
|
|
1799
2918
|
const sourcePoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.source_description, artifacts, "drag_source");
|
|
1800
2919
|
const targetPoint = await this.resolveVisualTargetPoint(job.job_id, screenshotPath, action.target_description, artifacts, "drag_target");
|
|
@@ -1804,18 +2923,29 @@ export class NativeMacOSJobExecutor {
|
|
|
1804
2923
|
if (!targetPoint) {
|
|
1805
2924
|
throw new Error(`Nao consegui localizar ${action.target_description} com confianca suficiente para concluir o arraste.`);
|
|
1806
2925
|
}
|
|
1807
|
-
await
|
|
2926
|
+
await reportActionProgress(`Arrastando ${action.source_description} para ${action.target_description}`);
|
|
1808
2927
|
await this.dragPoint(sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y);
|
|
1809
2928
|
resultPayload.last_drag = {
|
|
1810
2929
|
source: sourcePoint,
|
|
1811
2930
|
target: targetPoint,
|
|
1812
2931
|
};
|
|
2932
|
+
appendActionArtifact("interaction_result", {
|
|
2933
|
+
summary: `Arrastei ${action.source_description} para ${action.target_description}.`,
|
|
2934
|
+
});
|
|
1813
2935
|
completionNotes.push(`Arrastei ${action.source_description} para ${action.target_description}.`);
|
|
1814
2936
|
continue;
|
|
1815
2937
|
}
|
|
1816
|
-
await
|
|
2938
|
+
await reportActionProgress(`Abrindo ${action.url}${action.app ? ` em ${action.app}` : ""}`);
|
|
1817
2939
|
await this.openUrl(action.url, action.app);
|
|
1818
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
|
+
});
|
|
1819
2949
|
completionNotes.push(`${humanizeUrl(action.url)} foi aberto${action.app ? ` em ${action.app}` : ""}.`);
|
|
1820
2950
|
}
|
|
1821
2951
|
const summary = completionNotes.length > 0
|
|
@@ -1823,8 +2953,47 @@ export class NativeMacOSJobExecutor {
|
|
|
1823
2953
|
: (actions.length === 1
|
|
1824
2954
|
? this.describeAction(actions[0])
|
|
1825
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
|
+
}
|
|
1826
2968
|
resultPayload.summary = summary;
|
|
1827
|
-
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;
|
|
1828
2997
|
}
|
|
1829
2998
|
finally {
|
|
1830
2999
|
this.cancelledJobs.delete(job.job_id);
|
|
@@ -2315,7 +3484,10 @@ return appNames as text
|
|
|
2315
3484
|
status.summary = this.buildAppStatusSummary(status);
|
|
2316
3485
|
return status;
|
|
2317
3486
|
}
|
|
2318
|
-
async resolveFilesystemInspectPath(targetPath) {
|
|
3487
|
+
async resolveFilesystemInspectPath(targetPath, workspaceContext) {
|
|
3488
|
+
if (workspaceContext) {
|
|
3489
|
+
return assertPathInsideWorkspace(workspaceContext, targetPath);
|
|
3490
|
+
}
|
|
2319
3491
|
const expanded = expandUserPath(targetPath);
|
|
2320
3492
|
try {
|
|
2321
3493
|
await stat(expanded);
|
|
@@ -2342,8 +3514,8 @@ return appNames as text
|
|
|
2342
3514
|
return 0;
|
|
2343
3515
|
}
|
|
2344
3516
|
}
|
|
2345
|
-
async inspectFilesystemPath(targetPath, includeChildren = true, includePreview = false, limit = 8) {
|
|
2346
|
-
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);
|
|
2347
3519
|
const entryStat = await stat(resolved);
|
|
2348
3520
|
const itemName = path.basename(resolved) || resolved;
|
|
2349
3521
|
if (entryStat.isDirectory()) {
|
|
@@ -4606,25 +5778,68 @@ return {
|
|
|
4606
5778
|
if (targetApp !== "Safari") {
|
|
4607
5779
|
throw new Error("Leitura de pagina frontmost esta disponivel apenas para Safari no momento.");
|
|
4608
5780
|
}
|
|
4609
|
-
const script = `
|
|
4610
|
-
tell application "Safari"
|
|
4611
|
-
activate
|
|
4612
|
-
if (count of windows) = 0 then error "Safari nao possui janelas abertas."
|
|
4613
|
-
delay 1
|
|
4614
|
-
set jsCode to "(function(){const title=document.title||'';const url=location.href||'';const text=((document.body&&document.body.innerText)||'').trim().slice(0,12000);const isYouTubeMusic=location.hostname.includes('music.youtube.com');const isSpotify=location.hostname.includes('open.spotify.com');let playerButton=null;let playerTitle='';let playerState='';if(isYouTubeMusic){playerButton=document.querySelector('ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button');playerTitle=(Array.from(document.querySelectorAll('ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]')).map((node)=>((node&&node.textContent)||'').trim()).find(Boolean))||'';playerState=(playerButton&&((playerButton.getAttribute('title')||playerButton.getAttribute('aria-label')||playerButton.textContent)||'').trim())||'';}else if(isSpotify){const visible=(node)=>{if(!(node instanceof Element))return false;const rect=node.getBoundingClientRect();if(rect.width<4||rect.height<4)return false;const style=window.getComputedStyle(node);if(style.visibility==='hidden'||style.display==='none'||Number(style.opacity||'1')===0)return false;return rect.bottom>=0&&rect.right>=0&&rect.top<=window.innerHeight&&rect.left<=window.innerWidth;};const spotifyTitleCandidates=Array.from(document.querySelectorAll(\"[data-testid='nowplaying-track-link'], footer a[href*='/track/'], [data-testid='now-playing-widget'] a[href*='/track/'], a[href*='/track/']\")).filter((node)=>visible(node)).map((node)=>({node,text:((node&&node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>entry.text).sort((left,right)=>{const leftBottomBias=(left.rect.top>=window.innerHeight*0.72?200:0)+(left.rect.left<=window.innerWidth*0.45?120:0)+left.rect.top;const rightBottomBias=(right.rect.top>=window.innerHeight*0.72?200:0)+(right.rect.left<=window.innerWidth*0.45?120:0)+right.rect.top;return rightBottomBias-leftBottomBias;});playerTitle=(spotifyTitleCandidates[0]&&spotifyTitleCandidates[0].text)||'';playerButton=Array.from(document.querySelectorAll(\"footer button, [data-testid='control-button-playpause'], button[aria-label], button[title]\")).filter((node)=>visible(node)).map((node)=>({node,label:((node.getAttribute('aria-label')||node.getAttribute('title')||node.textContent)||'').trim(),rect:node.getBoundingClientRect()})).filter((entry)=>/play|pause|tocar|pausar|reproduzir/i.test(entry.label)).sort((left,right)=>{const leftScore=(left.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((left.rect.left+left.rect.width/2)-(window.innerWidth/2)));const rightScore=(right.rect.top>=window.innerHeight*0.72?200:0)+Math.max(0,200-Math.abs((right.rect.left+right.rect.width/2)-(window.innerWidth/2)));return rightScore-leftScore;})[0]?.node||null;playerState=(playerButton&&((playerButton.getAttribute('aria-label')||playerButton.getAttribute('title')||playerButton.textContent)||'').trim())||'';}return JSON.stringify({title,url,text,playerTitle,playerState});})();"
|
|
4615
|
-
set pageJson to do JavaScript jsCode in current tab of front window
|
|
4616
|
-
end tell
|
|
4617
|
-
return pageJson
|
|
4618
|
-
`;
|
|
4619
5781
|
try {
|
|
4620
|
-
const
|
|
4621
|
-
|
|
5782
|
+
const page = await this.runSafariJsonScript(`
|
|
5783
|
+
const title = document.title || "";
|
|
5784
|
+
const url = location.href || "";
|
|
5785
|
+
const text = ((document.body && document.body.innerText) || "").trim().slice(0, 12000);
|
|
5786
|
+
const isYouTubeMusic = location.hostname.includes("music.youtube.com");
|
|
5787
|
+
const isSpotify = location.hostname.includes("open.spotify.com");
|
|
5788
|
+
let playerButton = null;
|
|
5789
|
+
let playerTitle = "";
|
|
5790
|
+
let playerState = "";
|
|
5791
|
+
|
|
5792
|
+
if (isYouTubeMusic) {
|
|
5793
|
+
playerButton = document.querySelector("ytmusic-player-bar #play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button#play-pause-button, ytmusic-player-bar tp-yt-paper-icon-button.play-pause-button");
|
|
5794
|
+
playerTitle = (Array.from(document.querySelectorAll("ytmusic-player-bar .title, ytmusic-player-bar .content-info-wrapper .title, ytmusic-player-bar [slot=title]"))
|
|
5795
|
+
.map((node) => ((node && node.textContent) || "").trim())
|
|
5796
|
+
.find(Boolean)) || "";
|
|
5797
|
+
playerState = (playerButton && ((playerButton.getAttribute("title") || playerButton.getAttribute("aria-label") || playerButton.textContent) || "").trim()) || "";
|
|
5798
|
+
} else if (isSpotify) {
|
|
5799
|
+
const visible = (node) => {
|
|
5800
|
+
if (!(node instanceof Element)) return false;
|
|
5801
|
+
const rect = node.getBoundingClientRect();
|
|
5802
|
+
if (rect.width < 4 || rect.height < 4) return false;
|
|
5803
|
+
const style = window.getComputedStyle(node);
|
|
5804
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
5805
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
5806
|
+
};
|
|
5807
|
+
const spotifyTitleCandidates = Array.from(document.querySelectorAll("[data-testid='nowplaying-track-link'], footer a[href*='/track/'], [data-testid='now-playing-widget'] a[href*='/track/'], a[href*='/track/']"))
|
|
5808
|
+
.filter((node) => visible(node))
|
|
5809
|
+
.map((node) => ({ node, text: ((node && node.textContent) || "").trim(), rect: node.getBoundingClientRect() }))
|
|
5810
|
+
.filter((entry) => entry.text)
|
|
5811
|
+
.sort((left, right) => {
|
|
5812
|
+
const leftBottomBias = (left.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + (left.rect.left <= window.innerWidth * 0.45 ? 120 : 0) + left.rect.top;
|
|
5813
|
+
const rightBottomBias = (right.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + (right.rect.left <= window.innerWidth * 0.45 ? 120 : 0) + right.rect.top;
|
|
5814
|
+
return rightBottomBias - leftBottomBias;
|
|
5815
|
+
});
|
|
5816
|
+
playerTitle = (spotifyTitleCandidates[0] && spotifyTitleCandidates[0].text) || "";
|
|
5817
|
+
playerButton = Array.from(document.querySelectorAll("footer button, [data-testid='control-button-playpause'], button[aria-label], button[title]"))
|
|
5818
|
+
.filter((node) => visible(node))
|
|
5819
|
+
.map((node) => ({ node, label: ((node.getAttribute("aria-label") || node.getAttribute("title") || node.textContent) || "").trim(), rect: node.getBoundingClientRect() }))
|
|
5820
|
+
.filter((entry) => /play|pause|tocar|pausar|reproduzir/i.test(entry.label))
|
|
5821
|
+
.sort((left, right) => {
|
|
5822
|
+
const leftScore = (left.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + Math.max(0, 200 - Math.abs((left.rect.left + left.rect.width / 2) - (window.innerWidth / 2)));
|
|
5823
|
+
const rightScore = (right.rect.top >= window.innerHeight * 0.72 ? 200 : 0) + Math.max(0, 200 - Math.abs((right.rect.left + right.rect.width / 2) - (window.innerWidth / 2)));
|
|
5824
|
+
return rightScore - leftScore;
|
|
5825
|
+
})[0]?.node || null;
|
|
5826
|
+
playerState = (playerButton && ((playerButton.getAttribute("aria-label") || playerButton.getAttribute("title") || playerButton.textContent) || "").trim()) || "";
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
return {
|
|
5830
|
+
title,
|
|
5831
|
+
url,
|
|
5832
|
+
text,
|
|
5833
|
+
playerTitle,
|
|
5834
|
+
playerState,
|
|
5835
|
+
};
|
|
5836
|
+
`, {}, { activate: true });
|
|
4622
5837
|
return {
|
|
4623
|
-
title:
|
|
4624
|
-
url:
|
|
4625
|
-
text:
|
|
4626
|
-
playerTitle:
|
|
4627
|
-
playerState:
|
|
5838
|
+
title: page.title || "",
|
|
5839
|
+
url: page.url || "",
|
|
5840
|
+
text: page.text || "",
|
|
5841
|
+
playerTitle: page.playerTitle || "",
|
|
5842
|
+
playerState: page.playerState || "",
|
|
4628
5843
|
};
|
|
4629
5844
|
}
|
|
4630
5845
|
catch (error) {
|
|
@@ -5036,8 +6251,8 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5036
6251
|
}
|
|
5037
6252
|
return clipTextPreview(loaded.content || "(arquivo vazio)", maxChars);
|
|
5038
6253
|
}
|
|
5039
|
-
async readLocalFileSnapshot(filePath, chunkSizeChars = 4000) {
|
|
5040
|
-
const resolved = await this.resolveReadableFilePath(filePath);
|
|
6254
|
+
async readLocalFileSnapshot(filePath, chunkSizeChars = 4000, workspaceContext) {
|
|
6255
|
+
const resolved = await this.resolveReadableFilePath(filePath, workspaceContext);
|
|
5041
6256
|
const entryStat = await stat(resolved);
|
|
5042
6257
|
const loaded = await this.loadReadableFileContent(resolved);
|
|
5043
6258
|
const fileName = path.basename(resolved) || resolved;
|
|
@@ -5075,8 +6290,11 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5075
6290
|
summary: `Li ${filePath} por completo (${content.length} caracteres em ${chunks.length} bloco${chunks.length === 1 ? "" : "s"}).`,
|
|
5076
6291
|
};
|
|
5077
6292
|
}
|
|
5078
|
-
async
|
|
5079
|
-
|
|
6293
|
+
async resolveTrashTargetPath(targetPath, workspaceContext) {
|
|
6294
|
+
if (workspaceContext) {
|
|
6295
|
+
return assertPathInsideWorkspace(workspaceContext, targetPath);
|
|
6296
|
+
}
|
|
6297
|
+
const resolved = expandUserPath(targetPath);
|
|
5080
6298
|
try {
|
|
5081
6299
|
await stat(resolved);
|
|
5082
6300
|
return resolved;
|
|
@@ -5084,8 +6302,8 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5084
6302
|
catch {
|
|
5085
6303
|
// Continue into heuristic search below.
|
|
5086
6304
|
}
|
|
5087
|
-
const
|
|
5088
|
-
if (!
|
|
6305
|
+
const targetName = path.basename(resolved).trim();
|
|
6306
|
+
if (!targetName || targetName === "." || targetName === path.sep) {
|
|
5089
6307
|
return resolved;
|
|
5090
6308
|
}
|
|
5091
6309
|
const homeDir = os.homedir();
|
|
@@ -5097,11 +6315,14 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5097
6315
|
path.join(homeDir, "Documents"),
|
|
5098
6316
|
homeDir,
|
|
5099
6317
|
]);
|
|
5100
|
-
const found = await this.
|
|
6318
|
+
const found = await this.findPathByName(targetName, preferredRoots);
|
|
5101
6319
|
return found || resolved;
|
|
5102
6320
|
}
|
|
5103
|
-
async
|
|
5104
|
-
const
|
|
6321
|
+
async findPathByName(targetName, roots) {
|
|
6322
|
+
const normalizedTarget = normalizeText(targetName).replace(/\s+/g, " ").trim();
|
|
6323
|
+
if (!normalizedTarget) {
|
|
6324
|
+
return null;
|
|
6325
|
+
}
|
|
5105
6326
|
for (const root of roots) {
|
|
5106
6327
|
let rootStat;
|
|
5107
6328
|
try {
|
|
@@ -5127,16 +6348,17 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5127
6348
|
}
|
|
5128
6349
|
for (const entry of entries) {
|
|
5129
6350
|
const entryPath = path.join(current, entry.name);
|
|
6351
|
+
const normalizedEntryName = normalizeText(entry.name).replace(/\s+/g, " ").trim();
|
|
6352
|
+
if (normalizedEntryName === normalizedTarget) {
|
|
6353
|
+
return entryPath;
|
|
6354
|
+
}
|
|
5130
6355
|
if (entry.isDirectory()) {
|
|
5131
6356
|
if (!FILE_SEARCH_SKIP_DIRS.has(entry.name)) {
|
|
5132
6357
|
queue.push(entryPath);
|
|
5133
6358
|
}
|
|
5134
6359
|
continue;
|
|
5135
6360
|
}
|
|
5136
|
-
if (
|
|
5137
|
-
continue;
|
|
5138
|
-
}
|
|
5139
|
-
if (entry.name.toLowerCase() === target) {
|
|
6361
|
+
if (entry.isFile() && entry.name.toLowerCase() === targetName.toLowerCase()) {
|
|
5140
6362
|
return entryPath;
|
|
5141
6363
|
}
|
|
5142
6364
|
}
|
|
@@ -5144,337 +6366,1981 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5144
6366
|
}
|
|
5145
6367
|
return null;
|
|
5146
6368
|
}
|
|
5147
|
-
async
|
|
5148
|
-
const
|
|
5149
|
-
const
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
}
|
|
5154
|
-
return left.name.localeCompare(right.name);
|
|
5155
|
-
});
|
|
5156
|
-
const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.round(limit), 5_000)) : null;
|
|
5157
|
-
const selectedEntries = effectiveLimit ? sortedEntries.slice(0, effectiveLimit) : sortedEntries;
|
|
5158
|
-
const items = await Promise.all(selectedEntries.map(async (entry) => {
|
|
5159
|
-
const entryPath = path.join(resolved, entry.name);
|
|
5160
|
-
let sizeBytes;
|
|
5161
|
-
let modifiedAt;
|
|
6369
|
+
async resolveUniqueTrashDestination(targetPath) {
|
|
6370
|
+
const ext = path.extname(targetPath);
|
|
6371
|
+
const stem = ext ? targetPath.slice(0, -ext.length) : targetPath;
|
|
6372
|
+
let attempt = 0;
|
|
6373
|
+
let candidate = targetPath;
|
|
6374
|
+
while (true) {
|
|
5162
6375
|
try {
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
}
|
|
5167
|
-
modifiedAt = entryStat.mtime.toISOString();
|
|
6376
|
+
await stat(candidate);
|
|
6377
|
+
attempt += 1;
|
|
6378
|
+
candidate = `${stem} ${attempt + 1}${ext}`;
|
|
5168
6379
|
}
|
|
5169
6380
|
catch {
|
|
5170
|
-
|
|
6381
|
+
return candidate;
|
|
5171
6382
|
}
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
const
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
async movePathToTrashSnapshot(targetPath, workspaceContext) {
|
|
6386
|
+
const resolved = await this.resolveTrashTargetPath(targetPath, workspaceContext);
|
|
6387
|
+
const entryStat = await stat(resolved);
|
|
6388
|
+
const trashDir = path.join(os.homedir(), ".Trash");
|
|
6389
|
+
await mkdir(trashDir, { recursive: true });
|
|
6390
|
+
const name = path.basename(resolved) || resolved;
|
|
6391
|
+
const trashedPath = await this.resolveUniqueTrashDestination(path.join(trashDir, name));
|
|
6392
|
+
await rename(resolved, trashedPath);
|
|
6393
|
+
const kind = entryStat.isDirectory()
|
|
6394
|
+
? "directory"
|
|
6395
|
+
: entryStat.isFile()
|
|
6396
|
+
? "file"
|
|
6397
|
+
: "other";
|
|
5185
6398
|
return {
|
|
5186
6399
|
captured_at: new Date().toISOString(),
|
|
5187
|
-
path:
|
|
6400
|
+
path: targetPath,
|
|
5188
6401
|
resolved_path: resolved,
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
summary
|
|
6402
|
+
trashed_path: trashedPath,
|
|
6403
|
+
name,
|
|
6404
|
+
kind,
|
|
6405
|
+
size_bytes: entryStat.size,
|
|
6406
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
6407
|
+
summary: `${kind === "directory" ? "Mandei a pasta" : kind === "file" ? "Mandei o arquivo" : "Mandei o item"} ${name} para a Lixeira.`,
|
|
5195
6408
|
};
|
|
5196
6409
|
}
|
|
5197
|
-
async
|
|
5198
|
-
const
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
let total = 0;
|
|
5204
|
-
while (queue.length > 0) {
|
|
5205
|
-
const current = queue.shift();
|
|
5206
|
-
if (!current)
|
|
5207
|
-
continue;
|
|
5208
|
-
let entries;
|
|
6410
|
+
async resolveWritableTextFilePath(targetPath, filename, workspaceContext) {
|
|
6411
|
+
const expanded = workspaceContext
|
|
6412
|
+
? expandUserPathLike(targetPath, workspaceContext.defaultCwd)
|
|
6413
|
+
: expandUserPath(targetPath);
|
|
6414
|
+
const requestedFilename = filename ? sanitizeFileName(filename) : null;
|
|
6415
|
+
if (requestedFilename) {
|
|
5209
6416
|
try {
|
|
5210
|
-
|
|
6417
|
+
const existingStat = await stat(expanded);
|
|
6418
|
+
if (existingStat.isDirectory()) {
|
|
6419
|
+
const resolvedDirectoryPath = path.join(expanded, requestedFilename);
|
|
6420
|
+
return workspaceContext
|
|
6421
|
+
? assertPathInsideWorkspace(workspaceContext, resolvedDirectoryPath)
|
|
6422
|
+
: resolvedDirectoryPath;
|
|
6423
|
+
}
|
|
5211
6424
|
}
|
|
5212
6425
|
catch {
|
|
5213
|
-
|
|
6426
|
+
// Continue below and treat the target as a direct file path.
|
|
5214
6427
|
}
|
|
5215
|
-
|
|
5216
|
-
const
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
}
|
|
5221
|
-
continue;
|
|
5222
|
-
}
|
|
5223
|
-
if (!entry.isFile()) {
|
|
5224
|
-
continue;
|
|
5225
|
-
}
|
|
5226
|
-
if (normalizedExtensions.length > 0) {
|
|
5227
|
-
const entryExtension = path.extname(entry.name).toLowerCase().replace(/^\./, "");
|
|
5228
|
-
if (!normalizedExtensions.includes(entryExtension)) {
|
|
5229
|
-
continue;
|
|
5230
|
-
}
|
|
5231
|
-
}
|
|
5232
|
-
total += 1;
|
|
6428
|
+
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
6429
|
+
const resolvedDirectoryPath = path.join(expanded, requestedFilename);
|
|
6430
|
+
return workspaceContext
|
|
6431
|
+
? assertPathInsideWorkspace(workspaceContext, resolvedDirectoryPath)
|
|
6432
|
+
: resolvedDirectoryPath;
|
|
5233
6433
|
}
|
|
5234
6434
|
}
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
6435
|
+
try {
|
|
6436
|
+
const existingStat = await stat(expanded);
|
|
6437
|
+
if (existingStat.isDirectory()) {
|
|
6438
|
+
const fallbackDirectoryPath = path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
6439
|
+
return workspaceContext
|
|
6440
|
+
? assertPathInsideWorkspace(workspaceContext, fallbackDirectoryPath)
|
|
6441
|
+
: fallbackDirectoryPath;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
catch {
|
|
6445
|
+
// Continue below and treat the target as a direct file path.
|
|
6446
|
+
}
|
|
6447
|
+
if (String(targetPath || "").trimEnd().endsWith(path.sep)) {
|
|
6448
|
+
const fallbackDirectoryPath = path.join(expanded, sanitizeFileName("otto-note.txt"));
|
|
6449
|
+
return workspaceContext
|
|
6450
|
+
? assertPathInsideWorkspace(workspaceContext, fallbackDirectoryPath)
|
|
6451
|
+
: fallbackDirectoryPath;
|
|
6452
|
+
}
|
|
6453
|
+
return workspaceContext
|
|
6454
|
+
? assertPathInsideWorkspace(workspaceContext, expanded)
|
|
6455
|
+
: expanded;
|
|
6456
|
+
}
|
|
6457
|
+
async writeTextFileSnapshot(targetPath, text, filename, append = false, source, workspaceContext) {
|
|
6458
|
+
const resolved = await this.resolveWritableTextFilePath(targetPath, filename, workspaceContext);
|
|
6459
|
+
const parentDir = path.dirname(resolved);
|
|
6460
|
+
await mkdir(parentDir, { recursive: true });
|
|
6461
|
+
await writeFile(resolved, text, {
|
|
6462
|
+
encoding: "utf8",
|
|
6463
|
+
flag: append ? "a" : "w",
|
|
6464
|
+
});
|
|
6465
|
+
const entryStat = await stat(resolved);
|
|
6466
|
+
const name = path.basename(resolved) || resolved;
|
|
6467
|
+
const preview = clipTextPreview(String(text || "") || "(arquivo vazio)", 240);
|
|
5238
6468
|
return {
|
|
5239
|
-
|
|
5240
|
-
path:
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
6469
|
+
captured_at: new Date().toISOString(),
|
|
6470
|
+
path: targetPath,
|
|
6471
|
+
resolved_path: resolved,
|
|
6472
|
+
name,
|
|
6473
|
+
mime_type: "text/plain; charset=utf-8",
|
|
6474
|
+
size_bytes: entryStat.size,
|
|
6475
|
+
modified_at: entryStat.mtime.toISOString(),
|
|
6476
|
+
append,
|
|
6477
|
+
content_char_count: String(text || "").length,
|
|
6478
|
+
content_preview: preview,
|
|
6479
|
+
source: source || undefined,
|
|
6480
|
+
summary: `${append ? "Atualizei" : "Escrevi"} ${String(text || "").length} caractere${String(text || "").length === 1 ? "" : "s"} em ${resolved}.`,
|
|
5244
6481
|
};
|
|
5245
6482
|
}
|
|
5246
|
-
|
|
5247
|
-
const
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
for (const cpu of cpus) {
|
|
5251
|
-
idle += cpu.times.idle;
|
|
5252
|
-
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
|
|
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.");
|
|
5253
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;
|
|
5254
6494
|
return {
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
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}.`,
|
|
5259
6506
|
};
|
|
5260
6507
|
}
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
const totalDelta = Math.max(1, end.total - start.total);
|
|
5266
|
-
const idleDelta = Math.max(0, end.idle - start.idle);
|
|
5267
|
-
const idlePercent = roundMetric((idleDelta / totalDelta) * 100, 1);
|
|
5268
|
-
const usagePercent = roundMetric(Math.max(0, 100 - idlePercent), 1);
|
|
5269
|
-
const [load1m, load5m, load15m] = os.loadavg();
|
|
5270
|
-
return {
|
|
5271
|
-
usage_percent: usagePercent,
|
|
5272
|
-
idle_percent: idlePercent,
|
|
5273
|
-
logical_cores: end.logicalCores,
|
|
5274
|
-
model: end.model,
|
|
5275
|
-
load_average_1m: roundMetric(load1m, 2),
|
|
5276
|
-
load_average_5m: roundMetric(load5m, 2),
|
|
5277
|
-
load_average_15m: roundMetric(load15m, 2),
|
|
5278
|
-
};
|
|
6508
|
+
resolvePatchTargetPath(targetPath, resolvedCwd, workspaceContext) {
|
|
6509
|
+
return workspaceContext
|
|
6510
|
+
? assertPathInsideWorkspace(workspaceContext, targetPath, { baseCwd: resolvedCwd })
|
|
6511
|
+
: expandUserPathLike(targetPath, resolvedCwd);
|
|
5279
6512
|
}
|
|
5280
|
-
|
|
5281
|
-
const
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
let compressedBytes = 0;
|
|
5285
|
-
let swapUsedBytes = 0;
|
|
5286
|
-
try {
|
|
5287
|
-
const { stdout } = await this.runCommandCapture("/usr/bin/vm_stat", []);
|
|
5288
|
-
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/i);
|
|
5289
|
-
const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : 16384;
|
|
5290
|
-
const compressedMatch = stdout.match(/Pages occupied by compressor:\s+([0-9.]+)/i);
|
|
5291
|
-
if (compressedMatch) {
|
|
5292
|
-
compressedBytes = Math.round(Number(compressedMatch[1]) * pageSize);
|
|
5293
|
-
}
|
|
6513
|
+
countTextLines(value) {
|
|
6514
|
+
const normalized = String(value || "").replace(/\r\n/g, "\n");
|
|
6515
|
+
if (!normalized) {
|
|
6516
|
+
return 0;
|
|
5294
6517
|
}
|
|
5295
|
-
|
|
5296
|
-
|
|
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
|
+
};
|
|
5297
6543
|
}
|
|
5298
|
-
|
|
5299
|
-
const
|
|
5300
|
-
const
|
|
5301
|
-
if (
|
|
5302
|
-
|
|
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;
|
|
5303
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
|
+
};
|
|
5304
6565
|
}
|
|
5305
|
-
|
|
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
|
+
}
|
|
6758
|
+
resolveWriteTextFileContent(action) {
|
|
6759
|
+
const explicitText = [action.text, action.content, action.body]
|
|
6760
|
+
.map((value) => String(value || "").trim())
|
|
6761
|
+
.find(Boolean);
|
|
6762
|
+
if (explicitText) {
|
|
6763
|
+
return { content: explicitText };
|
|
6764
|
+
}
|
|
6765
|
+
const source = String(action.source || "").trim().toLowerCase();
|
|
6766
|
+
if (source === "last_page_text" || source === "last_page_summary") {
|
|
6767
|
+
const pageText = String(this.lastReadFrontmostPage?.text || "").trim();
|
|
6768
|
+
if (pageText) {
|
|
6769
|
+
return { content: pageText, source };
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
return null;
|
|
6773
|
+
}
|
|
6774
|
+
async resolveReadableFilePath(filePath, workspaceContext) {
|
|
6775
|
+
if (workspaceContext) {
|
|
6776
|
+
return assertPathInsideWorkspace(workspaceContext, filePath);
|
|
6777
|
+
}
|
|
6778
|
+
const resolved = expandUserPath(filePath);
|
|
6779
|
+
try {
|
|
6780
|
+
await stat(resolved);
|
|
6781
|
+
return resolved;
|
|
6782
|
+
}
|
|
6783
|
+
catch {
|
|
6784
|
+
// Continue into heuristic search below.
|
|
6785
|
+
}
|
|
6786
|
+
const filename = path.basename(resolved).trim();
|
|
6787
|
+
if (!filename || filename === "." || filename === path.sep) {
|
|
6788
|
+
return resolved;
|
|
6789
|
+
}
|
|
6790
|
+
const homeDir = os.homedir();
|
|
6791
|
+
const requestedDir = path.dirname(resolved);
|
|
6792
|
+
const preferredRoots = uniqueStrings([
|
|
6793
|
+
requestedDir && requestedDir !== homeDir ? requestedDir : null,
|
|
6794
|
+
path.join(homeDir, "Downloads"),
|
|
6795
|
+
path.join(homeDir, "Desktop"),
|
|
6796
|
+
path.join(homeDir, "Documents"),
|
|
6797
|
+
homeDir,
|
|
6798
|
+
]);
|
|
6799
|
+
const found = await this.findFileByName(filename, preferredRoots);
|
|
6800
|
+
return found || resolved;
|
|
6801
|
+
}
|
|
6802
|
+
async findFileByName(filename, roots) {
|
|
6803
|
+
const target = filename.toLowerCase();
|
|
6804
|
+
for (const root of roots) {
|
|
6805
|
+
let rootStat;
|
|
6806
|
+
try {
|
|
6807
|
+
rootStat = await stat(root);
|
|
6808
|
+
}
|
|
6809
|
+
catch {
|
|
6810
|
+
continue;
|
|
6811
|
+
}
|
|
6812
|
+
if (!rootStat.isDirectory()) {
|
|
6813
|
+
continue;
|
|
6814
|
+
}
|
|
6815
|
+
const queue = [root];
|
|
6816
|
+
while (queue.length > 0) {
|
|
6817
|
+
const current = queue.shift();
|
|
6818
|
+
if (!current)
|
|
6819
|
+
continue;
|
|
6820
|
+
let entries;
|
|
6821
|
+
try {
|
|
6822
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
6823
|
+
}
|
|
6824
|
+
catch {
|
|
6825
|
+
continue;
|
|
6826
|
+
}
|
|
6827
|
+
for (const entry of entries) {
|
|
6828
|
+
const entryPath = path.join(current, entry.name);
|
|
6829
|
+
if (entry.isDirectory()) {
|
|
6830
|
+
if (!FILE_SEARCH_SKIP_DIRS.has(entry.name)) {
|
|
6831
|
+
queue.push(entryPath);
|
|
6832
|
+
}
|
|
6833
|
+
continue;
|
|
6834
|
+
}
|
|
6835
|
+
if (!entry.isFile()) {
|
|
6836
|
+
continue;
|
|
6837
|
+
}
|
|
6838
|
+
if (entry.name.toLowerCase() === target) {
|
|
6839
|
+
return entryPath;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
return null;
|
|
6845
|
+
}
|
|
6846
|
+
async listLocalFilesSnapshot(directoryPath, limit, workspaceContext) {
|
|
6847
|
+
const resolved = workspaceContext
|
|
6848
|
+
? assertPathInsideWorkspace(workspaceContext, directoryPath)
|
|
6849
|
+
: expandUserPath(directoryPath);
|
|
6850
|
+
const allEntries = await readdir(resolved, { withFileTypes: true });
|
|
6851
|
+
const sortedEntries = allEntries.sort((left, right) => {
|
|
6852
|
+
if (left.isDirectory() !== right.isDirectory()) {
|
|
6853
|
+
return left.isDirectory() ? -1 : 1;
|
|
6854
|
+
}
|
|
6855
|
+
return left.name.localeCompare(right.name);
|
|
6856
|
+
});
|
|
6857
|
+
const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(1, Math.min(Math.round(limit), 5_000)) : null;
|
|
6858
|
+
const selectedEntries = effectiveLimit ? sortedEntries.slice(0, effectiveLimit) : sortedEntries;
|
|
6859
|
+
const items = await Promise.all(selectedEntries.map(async (entry) => {
|
|
6860
|
+
const entryPath = path.join(resolved, entry.name);
|
|
6861
|
+
let sizeBytes;
|
|
6862
|
+
let modifiedAt;
|
|
6863
|
+
try {
|
|
6864
|
+
const entryStat = await stat(entryPath);
|
|
6865
|
+
if (!entry.isDirectory()) {
|
|
6866
|
+
sizeBytes = entryStat.size;
|
|
6867
|
+
}
|
|
6868
|
+
modifiedAt = entryStat.mtime.toISOString();
|
|
6869
|
+
}
|
|
6870
|
+
catch {
|
|
6871
|
+
// Ignore stat failures and return the visible entry metadata we have.
|
|
6872
|
+
}
|
|
6873
|
+
return {
|
|
6874
|
+
name: entry.name,
|
|
6875
|
+
path: entryPath,
|
|
6876
|
+
kind: entry.isDirectory() ? "directory" : (entry.isFile() ? "file" : "other"),
|
|
6877
|
+
size_bytes: sizeBytes,
|
|
6878
|
+
modified_at: modifiedAt,
|
|
6879
|
+
};
|
|
6880
|
+
}));
|
|
6881
|
+
const summary = items.length === 0
|
|
6882
|
+
? `A pasta ${directoryPath} esta vazia.`
|
|
6883
|
+
: effectiveLimit && allEntries.length > items.length
|
|
6884
|
+
? `Listei ${items.length} itens visiveis em ${directoryPath} agora. A pasta tem ${allEntries.length} itens no total.`
|
|
6885
|
+
: `Listei ${items.length} itens de ${directoryPath} por completo.`;
|
|
6886
|
+
return {
|
|
6887
|
+
captured_at: new Date().toISOString(),
|
|
6888
|
+
path: directoryPath,
|
|
6889
|
+
resolved_path: resolved,
|
|
6890
|
+
name: path.basename(resolved) || resolved,
|
|
6891
|
+
item_count: items.length,
|
|
6892
|
+
total_item_count: allEntries.length,
|
|
6893
|
+
limit_applied: effectiveLimit || undefined,
|
|
6894
|
+
entries: items,
|
|
6895
|
+
summary,
|
|
6896
|
+
};
|
|
6897
|
+
}
|
|
6898
|
+
async countLocalFiles(directoryPath, extensions, recursive = true, workspaceContext) {
|
|
6899
|
+
const resolved = workspaceContext
|
|
6900
|
+
? assertPathInsideWorkspace(workspaceContext, directoryPath)
|
|
6901
|
+
: expandUserPath(directoryPath);
|
|
6902
|
+
const normalizedExtensions = Array.from(new Set((extensions || [])
|
|
6903
|
+
.map((extension) => String(extension || "").trim().toLowerCase().replace(/^\./, ""))
|
|
6904
|
+
.filter(Boolean)));
|
|
6905
|
+
const queue = [resolved];
|
|
6906
|
+
let total = 0;
|
|
6907
|
+
while (queue.length > 0) {
|
|
6908
|
+
const current = queue.shift();
|
|
6909
|
+
if (!current)
|
|
6910
|
+
continue;
|
|
6911
|
+
let entries;
|
|
6912
|
+
try {
|
|
6913
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
6914
|
+
}
|
|
6915
|
+
catch {
|
|
6916
|
+
continue;
|
|
6917
|
+
}
|
|
6918
|
+
for (const entry of entries) {
|
|
6919
|
+
const entryPath = path.join(current, entry.name);
|
|
6920
|
+
if (entry.isDirectory()) {
|
|
6921
|
+
if (recursive) {
|
|
6922
|
+
queue.push(entryPath);
|
|
6923
|
+
}
|
|
6924
|
+
continue;
|
|
6925
|
+
}
|
|
6926
|
+
if (!entry.isFile()) {
|
|
6927
|
+
continue;
|
|
6928
|
+
}
|
|
6929
|
+
if (normalizedExtensions.length > 0) {
|
|
6930
|
+
const entryExtension = path.extname(entry.name).toLowerCase().replace(/^\./, "");
|
|
6931
|
+
if (!normalizedExtensions.includes(entryExtension)) {
|
|
6932
|
+
continue;
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
total += 1;
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
const extensionsLabel = normalizedExtensions.length > 0
|
|
6939
|
+
? normalizedExtensions.map((extension) => `.${extension}`).join(", ")
|
|
6940
|
+
: "do tipo solicitado";
|
|
6941
|
+
return {
|
|
6942
|
+
total,
|
|
6943
|
+
path: directoryPath,
|
|
6944
|
+
extensions: normalizedExtensions,
|
|
6945
|
+
recursive,
|
|
6946
|
+
extensionsLabel,
|
|
6947
|
+
};
|
|
6948
|
+
}
|
|
6949
|
+
snapshotCpuTimes() {
|
|
6950
|
+
const cpus = os.cpus();
|
|
6951
|
+
let idle = 0;
|
|
6952
|
+
let total = 0;
|
|
6953
|
+
for (const cpu of cpus) {
|
|
6954
|
+
idle += cpu.times.idle;
|
|
6955
|
+
total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.irq + cpu.times.idle;
|
|
6956
|
+
}
|
|
6957
|
+
return {
|
|
6958
|
+
idle,
|
|
6959
|
+
total,
|
|
6960
|
+
model: cpus[0]?.model || "Apple Silicon",
|
|
6961
|
+
logicalCores: cpus.length || 0,
|
|
6962
|
+
};
|
|
6963
|
+
}
|
|
6964
|
+
async sampleCpuStatus() {
|
|
6965
|
+
const start = this.snapshotCpuTimes();
|
|
6966
|
+
await delay(320);
|
|
6967
|
+
const end = this.snapshotCpuTimes();
|
|
6968
|
+
const totalDelta = Math.max(1, end.total - start.total);
|
|
6969
|
+
const idleDelta = Math.max(0, end.idle - start.idle);
|
|
6970
|
+
const idlePercent = roundMetric((idleDelta / totalDelta) * 100, 1);
|
|
6971
|
+
const usagePercent = roundMetric(Math.max(0, 100 - idlePercent), 1);
|
|
6972
|
+
const [load1m, load5m, load15m] = os.loadavg();
|
|
6973
|
+
return {
|
|
6974
|
+
usage_percent: usagePercent,
|
|
6975
|
+
idle_percent: idlePercent,
|
|
6976
|
+
logical_cores: end.logicalCores,
|
|
6977
|
+
model: end.model,
|
|
6978
|
+
load_average_1m: roundMetric(load1m, 2),
|
|
6979
|
+
load_average_5m: roundMetric(load5m, 2),
|
|
6980
|
+
load_average_15m: roundMetric(load15m, 2),
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
async readMemoryStatus() {
|
|
6984
|
+
const totalBytes = os.totalmem();
|
|
6985
|
+
const freeBytes = os.freemem();
|
|
6986
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
6987
|
+
let compressedBytes = 0;
|
|
6988
|
+
let swapUsedBytes = 0;
|
|
6989
|
+
try {
|
|
6990
|
+
const { stdout } = await this.runCommandCapture("/usr/bin/vm_stat", []);
|
|
6991
|
+
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/i);
|
|
6992
|
+
const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : 16384;
|
|
6993
|
+
const compressedMatch = stdout.match(/Pages occupied by compressor:\s+([0-9.]+)/i);
|
|
6994
|
+
if (compressedMatch) {
|
|
6995
|
+
compressedBytes = Math.round(Number(compressedMatch[1]) * pageSize);
|
|
6996
|
+
}
|
|
6997
|
+
}
|
|
6998
|
+
catch {
|
|
6999
|
+
compressedBytes = 0;
|
|
7000
|
+
}
|
|
7001
|
+
try {
|
|
7002
|
+
const { stdout } = await this.runCommandCapture("/usr/sbin/sysctl", ["vm.swapusage"]);
|
|
7003
|
+
const usedMatch = stdout.match(/used = ([0-9.]+)([BKMGTP]+)/i);
|
|
7004
|
+
if (usedMatch) {
|
|
7005
|
+
swapUsedBytes = parseScaledBytes(usedMatch[1], usedMatch[2]);
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
catch {
|
|
5306
7009
|
swapUsedBytes = 0;
|
|
5307
7010
|
}
|
|
5308
|
-
const usedPercent = totalBytes > 0 ? roundMetric((usedBytes / totalBytes) * 100, 1) : 0;
|
|
5309
|
-
const pressure = (usedPercent >= 90 || swapUsedBytes >= 1.5 * 1024 ** 3)
|
|
5310
|
-
? "high"
|
|
5311
|
-
: (usedPercent >= 80 || swapUsedBytes >= 512 * 1024 ** 2 || compressedBytes >= 1024 ** 3)
|
|
5312
|
-
? "attention"
|
|
5313
|
-
: "normal";
|
|
7011
|
+
const usedPercent = totalBytes > 0 ? roundMetric((usedBytes / totalBytes) * 100, 1) : 0;
|
|
7012
|
+
const pressure = (usedPercent >= 90 || swapUsedBytes >= 1.5 * 1024 ** 3)
|
|
7013
|
+
? "high"
|
|
7014
|
+
: (usedPercent >= 80 || swapUsedBytes >= 512 * 1024 ** 2 || compressedBytes >= 1024 ** 3)
|
|
7015
|
+
? "attention"
|
|
7016
|
+
: "normal";
|
|
7017
|
+
return {
|
|
7018
|
+
total_bytes: totalBytes,
|
|
7019
|
+
used_bytes: usedBytes,
|
|
7020
|
+
free_bytes: freeBytes,
|
|
7021
|
+
used_percent: usedPercent,
|
|
7022
|
+
pressure,
|
|
7023
|
+
swap_used_bytes: swapUsedBytes || undefined,
|
|
7024
|
+
compressed_bytes: compressedBytes || undefined,
|
|
7025
|
+
};
|
|
7026
|
+
}
|
|
7027
|
+
async readDiskStatus() {
|
|
7028
|
+
const { stdout } = await this.runCommandCapture("/bin/df", ["-k", "/"]);
|
|
7029
|
+
const lines = stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
7030
|
+
if (lines.length < 2) {
|
|
7031
|
+
return undefined;
|
|
7032
|
+
}
|
|
7033
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
7034
|
+
if (parts.length < 6) {
|
|
7035
|
+
return undefined;
|
|
7036
|
+
}
|
|
7037
|
+
const totalBytes = Number(parts[1]) * 1024;
|
|
7038
|
+
const usedBytes = Number(parts[2]) * 1024;
|
|
7039
|
+
const availableBytes = Number(parts[3]) * 1024;
|
|
7040
|
+
const usedPercent = roundMetric(Number((parts[4] || "").replace("%", "")), 1);
|
|
7041
|
+
return {
|
|
7042
|
+
mount_path: parts[5] || "/",
|
|
7043
|
+
total_bytes: totalBytes,
|
|
7044
|
+
used_bytes: usedBytes,
|
|
7045
|
+
available_bytes: availableBytes,
|
|
7046
|
+
used_percent: usedPercent,
|
|
7047
|
+
available_gb: roundMetric(availableBytes / (1024 ** 3), 1),
|
|
7048
|
+
};
|
|
7049
|
+
}
|
|
7050
|
+
async readBatteryStatus() {
|
|
7051
|
+
try {
|
|
7052
|
+
const { stdout } = await this.runCommandCapture("/usr/bin/pmset", ["-g", "batt"]);
|
|
7053
|
+
const percentageMatch = stdout.match(/(\d+)%/);
|
|
7054
|
+
if (!percentageMatch) {
|
|
7055
|
+
return undefined;
|
|
7056
|
+
}
|
|
7057
|
+
const powerSourceMatch = stdout.match(/Now drawing from '([^']+)'/i);
|
|
7058
|
+
const powerSource = powerSourceMatch?.[1]?.trim() || "Unknown";
|
|
7059
|
+
const normalized = stdout.toLowerCase();
|
|
7060
|
+
const charging = normalized.includes("charging") || normalized.includes("charged") || powerSource.toLowerCase().includes("ac");
|
|
7061
|
+
return {
|
|
7062
|
+
percentage: Math.max(0, Math.min(100, Number(percentageMatch[1]))),
|
|
7063
|
+
charging,
|
|
7064
|
+
power_source: powerSource,
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
7067
|
+
catch {
|
|
7068
|
+
return undefined;
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
async readTopProcesses(limit = 4) {
|
|
7072
|
+
try {
|
|
7073
|
+
const { stdout } = await this.runCommandCapture("/bin/ps", ["-Ao", "pcpu,comm", "-r"]);
|
|
7074
|
+
const lines = stdout.trim().split(/\r?\n/).slice(1);
|
|
7075
|
+
return lines
|
|
7076
|
+
.map((line) => line.trim())
|
|
7077
|
+
.filter(Boolean)
|
|
7078
|
+
.slice(0, limit)
|
|
7079
|
+
.map((line) => {
|
|
7080
|
+
const match = line.match(/^([0-9.]+)\s+(.+)$/);
|
|
7081
|
+
if (!match)
|
|
7082
|
+
return null;
|
|
7083
|
+
const cpuPercent = roundMetric(Number(match[1]), 1);
|
|
7084
|
+
const command = match[2].trim().split("/").pop() || match[2].trim();
|
|
7085
|
+
return {
|
|
7086
|
+
name: command,
|
|
7087
|
+
cpu_percent: cpuPercent,
|
|
7088
|
+
};
|
|
7089
|
+
})
|
|
7090
|
+
.filter((item) => item !== null && item.cpu_percent > 0);
|
|
7091
|
+
}
|
|
7092
|
+
catch {
|
|
7093
|
+
return [];
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
buildSystemStatusSummary(status) {
|
|
7097
|
+
const parts = [];
|
|
7098
|
+
const warnings = [];
|
|
7099
|
+
if (status.cpu) {
|
|
7100
|
+
parts.push(`CPU em ${roundMetric(status.cpu.usage_percent)}%`);
|
|
7101
|
+
if (status.cpu.usage_percent >= 85) {
|
|
7102
|
+
warnings.push(`CPU em ${roundMetric(status.cpu.usage_percent)}%`);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
if (status.memory) {
|
|
7106
|
+
parts.push(`memoria em ${roundMetric(status.memory.used_percent)}%`);
|
|
7107
|
+
if (status.memory.pressure === "high") {
|
|
7108
|
+
warnings.push(`memoria pressionada (${roundMetric(status.memory.used_percent)}% e swap ativo)`);
|
|
7109
|
+
}
|
|
7110
|
+
else if (status.memory.pressure === "attention") {
|
|
7111
|
+
warnings.push(`memoria em ${roundMetric(status.memory.used_percent)}%`);
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
if (status.disk) {
|
|
7115
|
+
parts.push(`${formatBytesCompact(status.disk.available_bytes)} livres no disco`);
|
|
7116
|
+
if (status.disk.used_percent >= 90 || status.disk.available_bytes <= 15 * 1024 ** 3) {
|
|
7117
|
+
warnings.push(`pouco espaco livre (${formatBytesCompact(status.disk.available_bytes)})`);
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
if (status.battery) {
|
|
7121
|
+
parts.push(`bateria em ${status.battery.percentage}%${status.battery.charging ? " carregando" : ""}`);
|
|
7122
|
+
if (!status.battery.charging && status.battery.percentage <= 20) {
|
|
7123
|
+
warnings.push(`bateria em ${status.battery.percentage}%`);
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
let summary = warnings.length > 0
|
|
7127
|
+
? `Seu Mac esta operando, mas merece atencao em ${warnings.join(" e ")}.`
|
|
7128
|
+
: "No geral, seu Mac esta de boa.";
|
|
7129
|
+
if (parts.length > 0) {
|
|
7130
|
+
summary += ` Agora vejo ${parts.join(", ")}.`;
|
|
7131
|
+
}
|
|
7132
|
+
if (status.top_processes && status.top_processes.length > 0) {
|
|
7133
|
+
const topProcesses = status.top_processes
|
|
7134
|
+
.slice(0, 3)
|
|
7135
|
+
.map((item) => `${item.name} (${roundMetric(item.cpu_percent)}%)`)
|
|
7136
|
+
.join(", ");
|
|
7137
|
+
if (topProcesses) {
|
|
7138
|
+
summary += ` Maiores consumos agora: ${topProcesses}.`;
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
return summary;
|
|
7142
|
+
}
|
|
7143
|
+
async collectSystemStatus(sections, includeTopProcesses = true) {
|
|
7144
|
+
const requestedSections = sections && sections.length > 0
|
|
7145
|
+
? sections
|
|
7146
|
+
: ["cpu", "memory", "disk", "battery"];
|
|
7147
|
+
const uniqueSections = Array.from(new Set(requestedSections));
|
|
7148
|
+
const status = {
|
|
7149
|
+
captured_at: new Date().toISOString(),
|
|
7150
|
+
hostname: os.hostname(),
|
|
7151
|
+
platform: process.platform,
|
|
7152
|
+
requested_sections: uniqueSections,
|
|
7153
|
+
summary: "",
|
|
7154
|
+
};
|
|
7155
|
+
if (uniqueSections.includes("cpu")) {
|
|
7156
|
+
status.cpu = await this.sampleCpuStatus();
|
|
7157
|
+
}
|
|
7158
|
+
if (uniqueSections.includes("memory")) {
|
|
7159
|
+
status.memory = await this.readMemoryStatus();
|
|
7160
|
+
}
|
|
7161
|
+
if (uniqueSections.includes("disk")) {
|
|
7162
|
+
status.disk = await this.readDiskStatus();
|
|
7163
|
+
}
|
|
7164
|
+
if (uniqueSections.includes("battery")) {
|
|
7165
|
+
status.battery = await this.readBatteryStatus();
|
|
7166
|
+
}
|
|
7167
|
+
if (includeTopProcesses && (uniqueSections.includes("cpu") || uniqueSections.includes("memory"))) {
|
|
7168
|
+
const topProcesses = await this.readTopProcesses();
|
|
7169
|
+
if (topProcesses && topProcesses.length > 0) {
|
|
7170
|
+
status.top_processes = topProcesses;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
status.summary = this.buildSystemStatusSummary(status);
|
|
7174
|
+
return status;
|
|
7175
|
+
}
|
|
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;
|
|
5314
7690
|
return {
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
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}.`,
|
|
5322
7702
|
};
|
|
5323
7703
|
}
|
|
5324
|
-
async
|
|
5325
|
-
const
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
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
|
+
};
|
|
5329
7719
|
}
|
|
5330
|
-
const
|
|
5331
|
-
if (
|
|
5332
|
-
|
|
7720
|
+
const mergeArgs = ["merge"];
|
|
7721
|
+
if (options.ffOnly === true) {
|
|
7722
|
+
mergeArgs.push("--ff-only");
|
|
5333
7723
|
}
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
const
|
|
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);
|
|
5338
7761
|
return {
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
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}.`,
|
|
5345
7775
|
};
|
|
5346
7776
|
}
|
|
5347
|
-
async
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
const percentageMatch = stdout.match(/(\d+)%/);
|
|
5351
|
-
if (!percentageMatch) {
|
|
5352
|
-
return undefined;
|
|
5353
|
-
}
|
|
5354
|
-
const powerSourceMatch = stdout.match(/Now drawing from '([^']+)'/i);
|
|
5355
|
-
const powerSource = powerSourceMatch?.[1]?.trim() || "Unknown";
|
|
5356
|
-
const normalized = stdout.toLowerCase();
|
|
5357
|
-
const charging = normalized.includes("charging") || normalized.includes("charged") || powerSource.toLowerCase().includes("ac");
|
|
7777
|
+
async gitTagSnapshot(cwd, options, workspaceContext) {
|
|
7778
|
+
const repo = await this.probeGitRepository(cwd, workspaceContext);
|
|
7779
|
+
if (!repo.isRepo) {
|
|
5358
7780
|
return {
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
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.`,
|
|
5362
7791
|
};
|
|
5363
7792
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
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);
|
|
5366
7799
|
}
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
try {
|
|
5370
|
-
const { stdout } = await this.runCommandCapture("/bin/ps", ["-Ao", "pcpu,comm", "-r"]);
|
|
5371
|
-
const lines = stdout.trim().split(/\r?\n/).slice(1);
|
|
5372
|
-
return lines
|
|
5373
|
-
.map((line) => line.trim())
|
|
5374
|
-
.filter(Boolean)
|
|
5375
|
-
.slice(0, limit)
|
|
5376
|
-
.map((line) => {
|
|
5377
|
-
const match = line.match(/^([0-9.]+)\s+(.+)$/);
|
|
5378
|
-
if (!match)
|
|
5379
|
-
return null;
|
|
5380
|
-
const cpuPercent = roundMetric(Number(match[1]), 1);
|
|
5381
|
-
const command = match[2].trim().split("/").pop() || match[2].trim();
|
|
5382
|
-
return {
|
|
5383
|
-
name: command,
|
|
5384
|
-
cpu_percent: cpuPercent,
|
|
5385
|
-
};
|
|
5386
|
-
})
|
|
5387
|
-
.filter((item) => item !== null && item.cpu_percent > 0);
|
|
7800
|
+
else {
|
|
7801
|
+
tagArgs.push(options.name);
|
|
5388
7802
|
}
|
|
5389
|
-
|
|
5390
|
-
|
|
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
|
+
};
|
|
5391
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
|
+
};
|
|
5392
7848
|
}
|
|
5393
|
-
|
|
5394
|
-
const
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
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;
|
|
5400
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
|
+
};
|
|
5401
7904
|
}
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
warnings.push(`memoria em ${roundMetric(status.memory.used_percent)}%`);
|
|
5409
|
-
}
|
|
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");
|
|
5410
7911
|
}
|
|
5411
|
-
if (
|
|
5412
|
-
|
|
5413
|
-
if (status.disk.used_percent >= 90 || status.disk.available_bytes <= 15 * 1024 ** 3) {
|
|
5414
|
-
warnings.push(`pouco espaco livre (${formatBytesCompact(status.disk.available_bytes)})`);
|
|
5415
|
-
}
|
|
7912
|
+
if (pathspecs.length > 0) {
|
|
7913
|
+
addArgs.push("--", ...pathspecs);
|
|
5416
7914
|
}
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
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
|
+
};
|
|
5422
7934
|
}
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
if (parts.length > 0) {
|
|
5427
|
-
summary += ` Agora vejo ${parts.join(", ")}.`;
|
|
7935
|
+
const nameOnlyArgs = ["diff", "--cached", "--name-only"];
|
|
7936
|
+
if (pathspecs.length > 0) {
|
|
7937
|
+
nameOnlyArgs.push("--", ...pathspecs);
|
|
5428
7938
|
}
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
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
|
+
};
|
|
5437
7980
|
}
|
|
5438
|
-
|
|
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
|
+
};
|
|
5439
8027
|
}
|
|
5440
|
-
async
|
|
5441
|
-
const
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
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}` : ""}.`,
|
|
5451
8106
|
};
|
|
5452
|
-
|
|
5453
|
-
|
|
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
|
+
};
|
|
5454
8134
|
}
|
|
5455
|
-
|
|
5456
|
-
|
|
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
|
+
};
|
|
5457
8208
|
}
|
|
5458
|
-
|
|
5459
|
-
|
|
8209
|
+
const repoRoot = repoProbe.stdout.trim() || resolvedCwd;
|
|
8210
|
+
const diffArgs = ["diff", "--no-color"];
|
|
8211
|
+
if (staged) {
|
|
8212
|
+
diffArgs.push("--cached");
|
|
5460
8213
|
}
|
|
5461
|
-
if (
|
|
5462
|
-
|
|
8214
|
+
if (baseRef) {
|
|
8215
|
+
diffArgs.push(baseRef);
|
|
5463
8216
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
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;
|
|
5468
8305
|
}
|
|
5469
8306
|
}
|
|
5470
|
-
|
|
5471
|
-
|
|
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
|
+
};
|
|
5472
8338
|
}
|
|
5473
|
-
async runShellCommand(command, cwd) {
|
|
8339
|
+
async runShellCommand(command, cwd, workspaceContext) {
|
|
5474
8340
|
if (!isSafeShellCommand(command)) {
|
|
5475
8341
|
throw new Error("Nenhum comando shell foi informado para execucao local.");
|
|
5476
8342
|
}
|
|
5477
|
-
const resolvedCwd = cwd
|
|
8343
|
+
const resolvedCwd = this.resolveWorkspaceExecutionCwd(cwd || "", workspaceContext);
|
|
5478
8344
|
const { stdout, stderr } = await this.runCommandCapture("/bin/zsh", ["-lc", command], {
|
|
5479
8345
|
cwd: resolvedCwd,
|
|
5480
8346
|
});
|
|
@@ -5535,15 +8401,72 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5535
8401
|
if (action.type === "read_file") {
|
|
5536
8402
|
return `${action.path} foi lido no macOS`;
|
|
5537
8403
|
}
|
|
8404
|
+
if (action.type === "trash_path") {
|
|
8405
|
+
return `${action.path} foi movido para a Lixeira`;
|
|
8406
|
+
}
|
|
8407
|
+
if (action.type === "write_text_file") {
|
|
8408
|
+
return `Arquivo de texto escrito em ${action.filename ? `${action.path}/${action.filename}` : action.path}`;
|
|
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
|
+
}
|
|
5538
8449
|
if (action.type === "list_files") {
|
|
5539
8450
|
return `Arquivos listados em ${action.path}`;
|
|
5540
8451
|
}
|
|
8452
|
+
if (action.type === "delete_file") {
|
|
8453
|
+
return `Arquivo apagado em ${action.path}`;
|
|
8454
|
+
}
|
|
5541
8455
|
if (action.type === "count_files") {
|
|
5542
8456
|
return `Arquivos contados em ${action.path}`;
|
|
5543
8457
|
}
|
|
5544
8458
|
if (action.type === "system_status") {
|
|
5545
8459
|
return "Status do macOS coletado";
|
|
5546
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
|
+
}
|
|
5547
8470
|
if (action.type === "run_shell") {
|
|
5548
8471
|
return `Comando ${action.command} executado no macOS`;
|
|
5549
8472
|
}
|
|
@@ -5578,9 +8501,31 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5578
8501
|
});
|
|
5579
8502
|
this.activeChild = child;
|
|
5580
8503
|
try {
|
|
5581
|
-
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
8504
|
+
const { stdout, stderr, exitCode, timedOut } = await new Promise((resolve, reject) => {
|
|
5582
8505
|
let stdout = "";
|
|
5583
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
|
+
};
|
|
5584
8529
|
if (options?.stdin !== undefined) {
|
|
5585
8530
|
child.stdin.write(options.stdin);
|
|
5586
8531
|
child.stdin.end();
|
|
@@ -5595,21 +8540,36 @@ if let output = String(data: data, encoding: .utf8) {
|
|
|
5595
8540
|
stderr += String(chunk);
|
|
5596
8541
|
});
|
|
5597
8542
|
child.on("error", (error) => {
|
|
8543
|
+
if (settled) {
|
|
8544
|
+
return;
|
|
8545
|
+
}
|
|
8546
|
+
settled = true;
|
|
8547
|
+
clearTimer();
|
|
5598
8548
|
reject(error);
|
|
5599
8549
|
});
|
|
5600
8550
|
child.on("close", (code) => {
|
|
5601
|
-
if (
|
|
5602
|
-
|
|
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 });
|
|
5603
8563
|
return;
|
|
5604
8564
|
}
|
|
5605
|
-
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}`}`));
|
|
5606
8566
|
});
|
|
5607
8567
|
});
|
|
5608
8568
|
const stderrText = stderr.trim();
|
|
5609
8569
|
if (stderrText) {
|
|
5610
8570
|
console.warn(`[otto-bridge] ${command} stderr=${stderrText}`);
|
|
5611
8571
|
}
|
|
5612
|
-
return { stdout, stderr };
|
|
8572
|
+
return { stdout, stderr, exitCode, timedOut };
|
|
5613
8573
|
}
|
|
5614
8574
|
catch (error) {
|
|
5615
8575
|
const detail = error instanceof Error ? error.message : String(error);
|