@leg3ndy/otto-bridge 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -8
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/cli_terminal.js +490 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +18 -1
- package/dist/main.js +23 -0
- package/dist/runtime.js +91 -13
- package/dist/runtime_cli_client.js +18 -0
- package/dist/runtime_contract.js +516 -0
- package/dist/tool_catalog.js +148 -1
- package/dist/types.js +2 -2
- package/package.json +7 -2
- package/scripts/postinstall.mjs +35 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Para o estado atual da arquitetura, capacidades entregues, limitacoes e roadmap
|
|
|
15
15
|
|
|
16
16
|
Para o corte de arquitetura do `0.9.0`, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_0_9_0_RELEASE.md).
|
|
17
17
|
|
|
18
|
-
Para
|
|
18
|
+
Para a release atual `1.0.0`, com runtime agentico formal e CLI interativo do bridge, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_0_RELEASE.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_0_RELEASE.md).
|
|
19
19
|
|
|
20
20
|
## Distribuicao
|
|
21
21
|
|
|
@@ -29,6 +29,7 @@ O pacote ja esta estruturado para install via CLI:
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install -g @leg3ndy/otto-bridge
|
|
32
|
+
otto-bridge
|
|
32
33
|
otto-bridge status
|
|
33
34
|
otto-bridge version
|
|
34
35
|
```
|
|
@@ -37,14 +38,14 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
40
|
npm pack
|
|
40
|
-
npm install -g ./leg3ndy-otto-bridge-0.
|
|
41
|
+
npm install -g ./leg3ndy-otto-bridge-1.0.0.tgz
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
Na linha `0.
|
|
44
|
+
Na linha `1.0.0`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
44
45
|
|
|
45
|
-
No macOS, a linha `0.
|
|
46
|
+
No macOS, a linha `1.0.0` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
|
|
46
47
|
|
|
47
|
-
No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `0.
|
|
48
|
+
No nivel arquitetural, o `0.9.0` marcou a mudanca de papel do bridge: ele publica tools e resultados estruturados para o Otto, em vez de injetar resposta pronta como caminho principal do chat. O `1.0.0` oficializa isso como runtime agentico e adiciona uma interface terminal propria do bridge para setup e interacao local com o Otto.
|
|
48
49
|
|
|
49
50
|
## Publicacao
|
|
50
51
|
|
|
@@ -75,6 +76,23 @@ otto-bridge help
|
|
|
75
76
|
otto-bridge --help
|
|
76
77
|
```
|
|
77
78
|
|
|
79
|
+
### Abrir o hub interativo
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
otto-bridge
|
|
83
|
+
otto-bridge home
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Em TTY, o comando sem argumentos agora abre o hub interativo com banner, setup, status, extensoes e o `Otto Console`.
|
|
87
|
+
|
|
88
|
+
### Setup interativo
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
otto-bridge setup
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Esse fluxo pede `API base URL`, `pairing code`, nome do dispositivo e executor. Em install global interativo, o pacote tambem tenta abrir esse setup automaticamente quando ainda nao existe pairing salvo.
|
|
95
|
+
|
|
78
96
|
### Parear o dispositivo
|
|
79
97
|
|
|
80
98
|
```bash
|
|
@@ -112,9 +130,17 @@ O adapter `clawd-cursor` continua disponivel como override opcional:
|
|
|
112
130
|
otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
113
131
|
```
|
|
114
132
|
|
|
133
|
+
### Falar com o Otto no terminal
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
otto-bridge console
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
O console sobe o runtime local em background no mesmo processo, envia prompts naturais ao backend usando `device_token`, acompanha `device_job` por polling e resolve `confirm_required` diretamente no terminal.
|
|
140
|
+
|
|
115
141
|
### WhatsApp Web em background
|
|
116
142
|
|
|
117
|
-
Fluxo recomendado na linha `0.
|
|
143
|
+
Fluxo recomendado na linha `1.0.0`:
|
|
118
144
|
|
|
119
145
|
```bash
|
|
120
146
|
otto-bridge extensions --install whatsappweb
|
|
@@ -124,13 +150,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
124
150
|
|
|
125
151
|
O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
126
152
|
|
|
127
|
-
Contrato da linha `0.
|
|
153
|
+
Contrato da linha `1.0.0`:
|
|
128
154
|
|
|
129
155
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
130
156
|
- `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo, sem depender de uma aba aberta no Safari
|
|
131
157
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
132
158
|
|
|
133
|
-
## Handoff rapido da linha 0.
|
|
159
|
+
## Handoff rapido da linha 1.0.0
|
|
134
160
|
|
|
135
161
|
Ja fechado no codigo:
|
|
136
162
|
|
|
@@ -139,11 +165,44 @@ Ja fechado no codigo:
|
|
|
139
165
|
- resultado final dos `device_job` agora e persistido como contexto mais forte para o proximo turno do Otto
|
|
140
166
|
- prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
|
|
141
167
|
- runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
|
|
168
|
+
- o runtime agentico agora executa `execution_graph` granular por acao, em vez de colapsar a corrida local inteira num unico `tool_call`
|
|
169
|
+
- o protocolo do runtime agora publica `graph_id`/`step_id` nos eventos de job e aceita controles por step para confirmacao/cancelamento
|
|
170
|
+
- o executor `native-macos` agora reporta o step atual por acao e publica inline artifacts estruturados para resultados locais nao-uploadados
|
|
171
|
+
- o `runtime_contract` agora pode declarar `workspace_context` com roots/targets e descriptors de `instruction_bundle`, `repo_manifest`, `workspace_index`, `workspace_memory` e `workspace_policy` para jobs de arquivo e shell
|
|
172
|
+
- o bridge agora resolve `workspace_context` em paths absolutos, detecta `repo_root`, carrega `AGENTS.md` por precedencia e publica `instruction_bundle`, `repo_manifest`, `workspace_index`, `workspace_memory` e `workspace_policy` no resultado
|
|
173
|
+
- filesystem e shell locais agora respeitam os roots declarados do workspace, em vez de operar como superficie global quando o job vier escopado
|
|
174
|
+
- `workspace_policy` agora aplica perfis `observe_only`, `dev_assist`, `workspace_coding` e `release_operator` antes da execucao local de actions escopadas
|
|
175
|
+
- `write_json_file`, `mkdir`, `move_file` e `delete_file` agora fazem parte da familia inicial de file ops tipadas do workspace
|
|
176
|
+
- `workspace.patch`, `git.clone`, `git.fetch`, `git.checkout`, `git.rebase`, `git.merge`, `git.tag`, `git.status`, `git.diff`, `git.stage`, `git.commit`, `git.push` e `workspace.tests` agora fazem parte da primeira familia tipada de coding agent do catalogo local
|
|
177
|
+
- `apply_patch`, `git_status`, `git_diff`, `run_tests`, `git_clone`, `git_fetch`, `git_checkout`, `git_rebase`, `git_merge`, `git_tag`, `git_add`, `git_commit` e `git_push` agora executam dentro do workspace escopado e devolvem snapshots estruturados em vez de shell cru
|
|
178
|
+
- `apply_patch` entende o formato `*** Begin Patch ... *** End Patch`, devolve `patch_set` estruturado e continua bloqueando paths fora do workspace permitido
|
|
179
|
+
- `run_tests` agora aceita `command` explicito ou `profile` tipado (`pytest`, `npm_test`, `pnpm_test`, `yarn_test`, `bun_test`, `lint`, `build`) e devolve `resolved_command` no snapshot final
|
|
180
|
+
- `git_clone`, `git_fetch`, `git_checkout`, `git_rebase`, `git_merge`, `git_tag`, `git_add`, `git_commit` e `git_push` sobem com `requires_confirmation`, devolvem receipts estruturados (`git_clone_receipt`, `git_fetch_receipt`, `git_checkout_receipt`, `git_rebase_receipt`, `git_merge_receipt`, `git_tag_receipt`, `git_stage_receipt`, `git_commit_receipt`, `git_push_receipt`) e tratam `nothing_to_commit` / `Everything up-to-date` como resultado observavel do job
|
|
181
|
+
- `repo_manifest` e `workspace_index` agora saem com `scoped_root_path`, manifests/lockfiles, key files e extensoes dominantes para grounding leve de repo
|
|
182
|
+
- `workspace_memory` agora reaproveita o historico recente do mesmo `workspace_id` e volta atualizada no resultado do job para grounding curto entre execucoes
|
|
183
|
+
- `workspace_memory` agora tambem respeita governanca persistida no backend, com `retention_days`, `retention_job_limit`, `last_cleared_at` e `last_cleared_reason`, que o modal do Otto Bridge pode ajustar sem sair do runtime
|
|
184
|
+
- automacoes locais do canal `bridge` agora disparam `/v1/devices/automations/local/bridge/trigger` em cada tick, reaproveitando o mesmo runtime agentico do chat com `device_job`, approvals e replay auditaveis
|
|
185
|
+
- o `runtime_contract` agora tambem pode declarar `worker_profiles`, e o executor ecoa esse bundle em `worker_profiles`/artifact para deixar explicito quais perfis de leitura, mudanca, validacao e release foram ativados no job
|
|
186
|
+
- o `execution_graph` agora pode marcar `worker_id` por step, e o `runtime_contract` tambem pode declarar `worker_execution` com `budget_seconds`, `timeout_seconds`, `retry_limit` e `max_parallel_tasks` por worker para preparar fan-out controlado
|
|
187
|
+
- o backend agora deriva `runtime_workers` e `runtime_handoffs` persistidos do mesmo graph, incluindo `worker_run_id`, `fanout_level/wave/group` e retry por worker a partir de `retry_of_worker_run_id` / `retry_attempt`
|
|
188
|
+
- `git_diff` agora carrega `stat` e `diff_excerpt`, e o backend usa isso para grounding mais estrito de respostas sobre diff longo
|
|
189
|
+
- o `runtime_contract` agora tambem declara `command_packs` formais (`plan-fast`, `plan-deep`, `instruction-updater`), e o bridge ecoa esses modos como artifact estruturado para deixar claro qual intensidade de planejamento/manutencao foi aplicada no job
|
|
190
|
+
- o `instruction_updater` agora e um descriptor explicito do runtime, com `source_entrypoints`, `target_paths` e `recommended_validation_stage_ids` quando o plano toca `AGENTS.md`, `README.md` ou `docs/`
|
|
191
|
+
- `workspace.tests` agora consegue resolver `profile=auto` via `validation_ladder`, executando stages como `typecheck`, `build`, `node_test`, `lint` e `pytest` por stack, com snapshot agregado por etapa
|
|
192
|
+
- o executor `native-macos` agora publica `runtime_hook_trace` somando hooks de lifecycle e tool-use (`session_start`, `pre_tool_use`, `post_tool_use`, `validation_ladder_started/completed`, `session_end`), preparando enforcement, replay fino e metricas futuras
|
|
193
|
+
- o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para prompts locais pelo proprio terminal
|
|
194
|
+
- o backend agora expoe um caminho `device-auth` para o console do bridge submeter prompt e acompanhar approval/job usando apenas `device_token`
|
|
142
195
|
- o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
|
|
143
196
|
- `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
|
|
144
197
|
- `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
|
|
145
198
|
- o planner do bridge prioriza o modelo para escolher a tool certa pelo contexto; regex fica como fallback
|
|
146
199
|
|
|
200
|
+
### Testes locais do runtime
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm run test:runtime
|
|
204
|
+
```
|
|
205
|
+
|
|
147
206
|
Ainda precisa reteste em campo:
|
|
148
207
|
|
|
149
208
|
- fluxo completo do WhatsApp no helper `macos-helper`
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
function normalizePatchText(value) {
|
|
2
|
+
return value.replace(/\r\n/g, "\n");
|
|
3
|
+
}
|
|
4
|
+
function requirePatchPath(prefix, line) {
|
|
5
|
+
const targetPath = line.slice(prefix.length).trim();
|
|
6
|
+
if (!targetPath) {
|
|
7
|
+
throw new Error("O apply_patch recebeu uma operacao sem caminho de arquivo.");
|
|
8
|
+
}
|
|
9
|
+
return targetPath;
|
|
10
|
+
}
|
|
11
|
+
function parseUpdateHunks(lines) {
|
|
12
|
+
const hunks = [];
|
|
13
|
+
let current = null;
|
|
14
|
+
const ensureCurrentHunk = () => {
|
|
15
|
+
if (!current) {
|
|
16
|
+
current = { header: "@@", lines: [] };
|
|
17
|
+
hunks.push(current);
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
};
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (!line.trim()) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (line.startsWith("@@")) {
|
|
26
|
+
current = { header: line.trim(), lines: [] };
|
|
27
|
+
hunks.push(current);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (line === "*** End of File") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const prefix = line.slice(0, 1);
|
|
34
|
+
const hunk = ensureCurrentHunk();
|
|
35
|
+
if (prefix === " ") {
|
|
36
|
+
hunk.lines.push({ kind: "context", text: line.slice(1) });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (prefix === "+") {
|
|
40
|
+
hunk.lines.push({ kind: "add", text: line.slice(1) });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (prefix === "-") {
|
|
44
|
+
hunk.lines.push({ kind: "delete", text: line.slice(1) });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Linha invalida no apply_patch: ${line}`);
|
|
48
|
+
}
|
|
49
|
+
return hunks.filter((item) => item.lines.length > 0);
|
|
50
|
+
}
|
|
51
|
+
export function parseStructuredPatch(patchText) {
|
|
52
|
+
const normalized = normalizePatchText(String(patchText || ""));
|
|
53
|
+
const lines = normalized.split("\n");
|
|
54
|
+
const operations = [];
|
|
55
|
+
let index = 0;
|
|
56
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
if (lines[index]?.trim() !== "*** Begin Patch") {
|
|
60
|
+
throw new Error("O apply_patch exige o formato *** Begin Patch ... *** End Patch.");
|
|
61
|
+
}
|
|
62
|
+
index += 1;
|
|
63
|
+
while (index < lines.length) {
|
|
64
|
+
const line = lines[index]?.trimEnd() || "";
|
|
65
|
+
if (!line.trim()) {
|
|
66
|
+
index += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line.trim() === "*** End Patch") {
|
|
70
|
+
return {
|
|
71
|
+
operations,
|
|
72
|
+
patch_char_count: normalized.length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith("*** Add File: ")) {
|
|
76
|
+
const targetPath = requirePatchPath("*** Add File: ", line);
|
|
77
|
+
index += 1;
|
|
78
|
+
const contentLines = [];
|
|
79
|
+
while (index < lines.length) {
|
|
80
|
+
const currentLine = lines[index] || "";
|
|
81
|
+
if (currentLine.startsWith("*** ")) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (!currentLine.startsWith("+")) {
|
|
85
|
+
throw new Error(`Linha invalida em Add File: ${currentLine}`);
|
|
86
|
+
}
|
|
87
|
+
contentLines.push(currentLine.slice(1));
|
|
88
|
+
index += 1;
|
|
89
|
+
}
|
|
90
|
+
operations.push({
|
|
91
|
+
type: "add",
|
|
92
|
+
path: targetPath,
|
|
93
|
+
content_lines: contentLines,
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (line.startsWith("*** Delete File: ")) {
|
|
98
|
+
operations.push({
|
|
99
|
+
type: "delete",
|
|
100
|
+
path: requirePatchPath("*** Delete File: ", line),
|
|
101
|
+
});
|
|
102
|
+
index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith("*** Update File: ")) {
|
|
106
|
+
const targetPath = requirePatchPath("*** Update File: ", line);
|
|
107
|
+
index += 1;
|
|
108
|
+
let moveTo;
|
|
109
|
+
if ((lines[index] || "").startsWith("*** Move to: ")) {
|
|
110
|
+
moveTo = requirePatchPath("*** Move to: ", lines[index] || "");
|
|
111
|
+
index += 1;
|
|
112
|
+
}
|
|
113
|
+
const updateLines = [];
|
|
114
|
+
while (index < lines.length) {
|
|
115
|
+
const currentLine = lines[index] || "";
|
|
116
|
+
if (currentLine.startsWith("*** Add File: ")
|
|
117
|
+
|| currentLine.startsWith("*** Delete File: ")
|
|
118
|
+
|| currentLine.startsWith("*** Update File: ")
|
|
119
|
+
|| currentLine.trim() === "*** End Patch") {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
updateLines.push(currentLine);
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
operations.push({
|
|
126
|
+
type: "update",
|
|
127
|
+
path: targetPath,
|
|
128
|
+
move_to: moveTo,
|
|
129
|
+
hunks: parseUpdateHunks(updateLines),
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Nao reconheci a operacao do apply_patch: ${line}`);
|
|
134
|
+
}
|
|
135
|
+
throw new Error("O apply_patch nao encontrou o marcador *** End Patch.");
|
|
136
|
+
}
|
|
137
|
+
export function collectStructuredPatchTargets(patchText) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = parseStructuredPatch(patchText);
|
|
140
|
+
const targets = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
for (const operation of parsed.operations) {
|
|
143
|
+
const pushTarget = (targetPath, accessMode) => {
|
|
144
|
+
const key = `${accessMode}:${targetPath}`;
|
|
145
|
+
if (!targetPath || seen.has(key)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
seen.add(key);
|
|
149
|
+
targets.push({ path: targetPath, accessMode });
|
|
150
|
+
};
|
|
151
|
+
if (operation.type === "add") {
|
|
152
|
+
pushTarget(operation.path, "write");
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (operation.type === "delete") {
|
|
156
|
+
pushTarget(operation.path, "delete");
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
pushTarget(operation.path, "write");
|
|
160
|
+
if (operation.move_to) {
|
|
161
|
+
pushTarget(operation.move_to, "write");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return targets;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function structuredPatchHasDelete(patchText) {
|
|
171
|
+
return collectStructuredPatchTargets(patchText).some((item) => item.accessMode === "delete");
|
|
172
|
+
}
|
|
173
|
+
function splitTextForPatch(value) {
|
|
174
|
+
const normalized = normalizePatchText(value);
|
|
175
|
+
if (!normalized) {
|
|
176
|
+
return { lines: [], trailingNewline: false };
|
|
177
|
+
}
|
|
178
|
+
const trailingNewline = normalized.endsWith("\n");
|
|
179
|
+
const lines = normalized.split("\n");
|
|
180
|
+
if (trailingNewline) {
|
|
181
|
+
lines.pop();
|
|
182
|
+
}
|
|
183
|
+
return { lines, trailingNewline };
|
|
184
|
+
}
|
|
185
|
+
function joinPatchedText(lines, trailingNewline) {
|
|
186
|
+
if (lines.length === 0) {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
return `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
190
|
+
}
|
|
191
|
+
function findMatchingSegment(lines, targetLines, startIndex) {
|
|
192
|
+
if (targetLines.length === 0) {
|
|
193
|
+
return startIndex;
|
|
194
|
+
}
|
|
195
|
+
for (let index = startIndex; index <= lines.length - targetLines.length; index += 1) {
|
|
196
|
+
let matches = true;
|
|
197
|
+
for (let offset = 0; offset < targetLines.length; offset += 1) {
|
|
198
|
+
if (lines[index + offset] !== targetLines[offset]) {
|
|
199
|
+
matches = false;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (matches) {
|
|
204
|
+
return index;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (startIndex > 0) {
|
|
208
|
+
return findMatchingSegment(lines, targetLines, 0);
|
|
209
|
+
}
|
|
210
|
+
return -1;
|
|
211
|
+
}
|
|
212
|
+
export function applyStructuredUpdateToText(originalText, operation) {
|
|
213
|
+
const { lines, trailingNewline } = splitTextForPatch(originalText);
|
|
214
|
+
let cursor = 0;
|
|
215
|
+
let addedLineCount = 0;
|
|
216
|
+
let deletedLineCount = 0;
|
|
217
|
+
for (const hunk of operation.hunks) {
|
|
218
|
+
const oldLines = hunk.lines
|
|
219
|
+
.filter((line) => line.kind !== "add")
|
|
220
|
+
.map((line) => line.text);
|
|
221
|
+
const newLines = hunk.lines
|
|
222
|
+
.filter((line) => line.kind !== "delete")
|
|
223
|
+
.map((line) => line.text);
|
|
224
|
+
const matchIndex = oldLines.length === 0
|
|
225
|
+
? cursor
|
|
226
|
+
: findMatchingSegment(lines, oldLines, cursor);
|
|
227
|
+
if (matchIndex < 0) {
|
|
228
|
+
throw new Error(`Nao consegui localizar o contexto do patch para ${operation.path}.`);
|
|
229
|
+
}
|
|
230
|
+
lines.splice(matchIndex, oldLines.length, ...newLines);
|
|
231
|
+
cursor = matchIndex + newLines.length;
|
|
232
|
+
addedLineCount += hunk.lines.filter((line) => line.kind === "add").length;
|
|
233
|
+
deletedLineCount += hunk.lines.filter((line) => line.kind === "delete").length;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
text: joinPatchedText(lines, trailingNewline),
|
|
237
|
+
addedLineCount,
|
|
238
|
+
deletedLineCount,
|
|
239
|
+
};
|
|
240
|
+
}
|