@leg3ndy/otto-bridge 0.9.2 → 1.0.1
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 +78 -17
- package/dist/agentic_runtime/patch/structured_patch.js +240 -0
- package/dist/agentic_runtime/workspace/manager.js +1044 -0
- package/dist/chat_cli_client.js +91 -0
- package/dist/cli_terminal.js +668 -0
- package/dist/executors/native_macos.js +2778 -115
- package/dist/local_automations.js +33 -11
- package/dist/main.js +25 -3
- package/dist/runtime.js +136 -32
- 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.1`, com runtime agentico formal, hub terminal e console alinhado ao Otto da web, veja [`leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.md`](../leg3ndy-ai-backend/docs/OTTO_BRIDGE_1_0_1_PATCH.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.1.tgz
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
Na linha `0.
|
|
44
|
+
Na linha `1.0.1`, `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.1` 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` oficializou isso como runtime agentico; o `1.0.1` consolida o hub terminal como fluxo principal e alinha o console ao comportamento normal do Otto antes de fazer handoff local.
|
|
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 e ligar o runtime
|
|
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`. Se ja existir pairing salvo, o próprio `otto-bridge` sobe o runtime local automaticamente e mostra o estado da conexão no hub.
|
|
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
|
|
@@ -92,29 +110,39 @@ Opcoes suportadas:
|
|
|
92
110
|
|
|
93
111
|
No macOS, o caminho recomendado agora e o executor nativo do Otto Bridge. Se nenhum `--executor` for informado, o `pair` usa `native-macos` por padrao no Mac.
|
|
94
112
|
|
|
95
|
-
###
|
|
113
|
+
### Runtime principal do bridge
|
|
96
114
|
|
|
97
115
|
```bash
|
|
98
|
-
otto-bridge
|
|
116
|
+
otto-bridge
|
|
99
117
|
```
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
Esse agora e o fluxo principal do produto: o hub abre, conecta o runtime local e deixa o Otto pronto para handoff e approvals sem depender de um `run` separado.
|
|
102
120
|
|
|
103
|
-
|
|
121
|
+
Se precisar do executor nativo no macOS sem reparar, atualize o pairing/config e reabra o hub:
|
|
104
122
|
|
|
105
123
|
```bash
|
|
106
|
-
otto-bridge
|
|
124
|
+
otto-bridge setup
|
|
107
125
|
```
|
|
108
126
|
|
|
109
|
-
|
|
127
|
+
`otto-bridge run` continua existindo apenas como alias legado/headless para compatibilidade operacional.
|
|
128
|
+
|
|
129
|
+
O adapter `clawd-cursor` continua disponivel como override opcional no pairing legado:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
otto-bridge pair --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Falar com o Otto no terminal
|
|
110
136
|
|
|
111
137
|
```bash
|
|
112
|
-
otto-bridge
|
|
138
|
+
otto-bridge console
|
|
113
139
|
```
|
|
114
140
|
|
|
141
|
+
O console usa a mesma sessão local já ligada pelo `otto-bridge`, envia prompts naturais ao backend usando `device_token`, respeita quota/plano do usuário e só vira handoff local quando o pedido realmente tiver cara de ação no computador. Quando houver `device_job`, ele acompanha polling e resolve `confirm_required` no terminal.
|
|
142
|
+
|
|
115
143
|
### WhatsApp Web em background
|
|
116
144
|
|
|
117
|
-
Fluxo recomendado na linha `0.
|
|
145
|
+
Fluxo recomendado na linha `1.0.1`:
|
|
118
146
|
|
|
119
147
|
```bash
|
|
120
148
|
otto-bridge extensions --install whatsappweb
|
|
@@ -124,13 +152,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
124
152
|
|
|
125
153
|
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
154
|
|
|
127
|
-
Contrato da linha `0.
|
|
155
|
+
Contrato da linha `1.0.1`:
|
|
128
156
|
|
|
129
157
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
130
|
-
- `otto-bridge
|
|
131
|
-
- ao
|
|
158
|
+
- `otto-bridge`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime do hub estiver ativo, sem depender de uma aba aberta no Safari
|
|
159
|
+
- ao fechar o `otto-bridge`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
132
160
|
|
|
133
|
-
## Handoff rapido da linha 0.
|
|
161
|
+
## Handoff rapido da linha 1.0.1
|
|
134
162
|
|
|
135
163
|
Ja fechado no codigo:
|
|
136
164
|
|
|
@@ -139,11 +167,44 @@ Ja fechado no codigo:
|
|
|
139
167
|
- resultado final dos `device_job` agora e persistido como contexto mais forte para o proximo turno do Otto
|
|
140
168
|
- prompt bridge-aware no chat normal para ajudar o Otto a responder com base no que realmente aconteceu no device
|
|
141
169
|
- runtime local agora publica `local_tools` para o Otto/backend saberem exatamente o que o device consegue fazer
|
|
170
|
+
- o runtime agentico agora executa `execution_graph` granular por acao, em vez de colapsar a corrida local inteira num unico `tool_call`
|
|
171
|
+
- o protocolo do runtime agora publica `graph_id`/`step_id` nos eventos de job e aceita controles por step para confirmacao/cancelamento
|
|
172
|
+
- o executor `native-macos` agora reporta o step atual por acao e publica inline artifacts estruturados para resultados locais nao-uploadados
|
|
173
|
+
- 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
|
|
174
|
+
- 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
|
|
175
|
+
- filesystem e shell locais agora respeitam os roots declarados do workspace, em vez de operar como superficie global quando o job vier escopado
|
|
176
|
+
- `workspace_policy` agora aplica perfis `observe_only`, `dev_assist`, `workspace_coding` e `release_operator` antes da execucao local de actions escopadas
|
|
177
|
+
- `write_json_file`, `mkdir`, `move_file` e `delete_file` agora fazem parte da familia inicial de file ops tipadas do workspace
|
|
178
|
+
- `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
|
|
179
|
+
- `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
|
|
180
|
+
- `apply_patch` entende o formato `*** Begin Patch ... *** End Patch`, devolve `patch_set` estruturado e continua bloqueando paths fora do workspace permitido
|
|
181
|
+
- `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
|
|
182
|
+
- `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
|
|
183
|
+
- `repo_manifest` e `workspace_index` agora saem com `scoped_root_path`, manifests/lockfiles, key files e extensoes dominantes para grounding leve de repo
|
|
184
|
+
- `workspace_memory` agora reaproveita o historico recente do mesmo `workspace_id` e volta atualizada no resultado do job para grounding curto entre execucoes
|
|
185
|
+
- `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
|
|
186
|
+
- 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
|
|
187
|
+
- 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
|
|
188
|
+
- 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
|
|
189
|
+
- 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`
|
|
190
|
+
- `git_diff` agora carrega `stat` e `diff_excerpt`, e o backend usa isso para grounding mais estrito de respostas sobre diff longo
|
|
191
|
+
- 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
|
|
192
|
+
- 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/`
|
|
193
|
+
- `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
|
|
194
|
+
- 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
|
|
195
|
+
- o bridge agora possui um CLI interativo proprio com setup inicial, hub terminal e `Otto Console` para conversar com o Otto pelo proprio terminal
|
|
196
|
+
- o backend agora expoe caminhos `device-auth` para o console do bridge usar chat com quota/plano (`/v1/devices/cli/chat/completions`) e acompanhar approval/job usando apenas `device_token`
|
|
142
197
|
- o caminho principal do bridge usa `summary`/`narration_context` no lugar de resposta automatica pronta
|
|
143
198
|
- `read_file` agora entrega conteudo completo segmentado em `content_chunks` para o Otto
|
|
144
199
|
- `list_files` sem `limit` agora lista o diretorio inteiro, sem fallback silencioso para 40 itens
|
|
145
200
|
- o planner do bridge prioriza o modelo para escolher a tool certa pelo contexto; regex fica como fallback
|
|
146
201
|
|
|
202
|
+
### Testes locais do runtime
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm run test:runtime
|
|
206
|
+
```
|
|
207
|
+
|
|
147
208
|
Ainda precisa reteste em campo:
|
|
148
209
|
|
|
149
210
|
- 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
|
+
}
|