@aksp/opencrew 1.7.1 → 1.8.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/CHANGELOG.md +60 -0
- package/README.md +52 -2
- package/package.json +1 -1
- package/templates/AGENTS.md +3 -0
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/entrega.prompt.md +131 -0
- package/templates/_opencrew/core/runner.pipeline.md +13 -4
- package/templates/_opencrew/core/scripts/entrega/argumentos.mjs +46 -0
- package/templates/_opencrew/core/scripts/entrega/canais.mjs +51 -0
- package/templates/_opencrew/core/scripts/entrega/fora.mjs +49 -0
- package/templates/_opencrew/core/scripts/entrega/gravar.mjs +83 -0
- package/templates/_opencrew/core/scripts/entrega/leiame.mjs +89 -0
- package/templates/_opencrew/core/scripts/entrega/leitor.mjs +120 -0
- package/templates/_opencrew/core/scripts/entrega/longas.mjs +109 -0
- package/templates/_opencrew/core/scripts/entrega/nomes.mjs +78 -0
- package/templates/_opencrew/core/scripts/entrega/passos.mjs +99 -0
- package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +87 -0
- package/templates/_opencrew/core/scripts/entrega/separar.mjs +103 -0
- package/templates/_opencrew/core/scripts/entrega/texto.mjs +95 -0
- package/templates/_opencrew/core/scripts/entregar.mjs +141 -0
- package/templates/_opencrew/core/scripts/verificar/pecas.mjs +4 -4
- package/templates/_opencrew/core/scripts/verificar/secoes.mjs +22 -2
- package/templates/_opencrew/core/scripts/verificar.mjs +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,66 @@
|
|
|
3
3
|
All notable changes to opencrew are documented here.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
|
|
6
|
+
## [1.8.0] — 2026-10-07
|
|
7
|
+
|
|
8
|
+
Fase U3a, fatia 1 "Pasta de entrega" (`specs/fase-u3a1-pasta-de-entrega.md`). Chega a quem já usa
|
|
9
|
+
com um `npx @aksp/opencrew@latest update`, e funciona nas crews que já existem, sem mexer nelas.
|
|
10
|
+
|
|
11
|
+
Ainda não nesta versão: PDF e "posts formatados" não são gerados pela entrega (`artigo.md`,
|
|
12
|
+
`corpo.md` e os roteiros saem em markdown, e o LEIA-ME ensina a salvar como PDF pelo "Imprimir");
|
|
13
|
+
copiar a entrega para uma pasta do projeto, registrar "entregar assim mesmo" e marcar "já
|
|
14
|
+
publicado" ficam para a 1.9.0. O LEIA-ME e os nomes dos arquivos são só em português.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Pasta `entrega/`: o que você usa, separado por canal.** Depois da aprovação final, a crew
|
|
18
|
+
monta `crews/<crew>/output/<execução>/entrega/` com uma pasta por canal (`instagram/`,
|
|
19
|
+
`linkedin/`, `blog/`, `email/`, `whatsapp/`, `twitter/`, `youtube/`; só os que a execução tem).
|
|
20
|
+
Antes, você recebia a pasta da execução com `v1`, `v2`, relatórios e textos cheios de `#`, `**`
|
|
21
|
+
e rótulos.
|
|
22
|
+
- **Texto pronto para colar.** `legenda.txt`, `post.txt` e `tweet.txt` saem sem `#`, `**`, rótulos
|
|
23
|
+
nem recados internos, com as hashtags no fim. O primeiro comentário do LinkedIn vem em arquivo
|
|
24
|
+
à parte; a thread, em `tweet-1.txt`, `tweet-2.txt`…; o blog, em `seo.txt` (título, meta
|
|
25
|
+
description, palavra-chave, slug) e `artigo.md`; o e-mail, em `assunto.txt`, `previa.txt` e
|
|
26
|
+
`corpo.md`; o WhatsApp, em `mensagem.txt`. As imagens vão para a pasta do canal com o nome
|
|
27
|
+
original, sem alteração; o HTML dos slides, para `editaveis/`.
|
|
28
|
+
- **`LEIA-ME.md` em cada entrega.** Diz o que fazer com cada arquivo, canal por canal, em passos
|
|
29
|
+
numerados; marca cada canal como "Pronto" ou "Não está pronto"; lista em "Antes de usar" o que
|
|
30
|
+
falta; e diz o que não foi conferido (links e fatos, texto dentro das imagens, aparência final
|
|
31
|
+
em cada rede).
|
|
32
|
+
- **O que não está pronto fica marcado.** Texto com `[PREENCHER]`, acima de um limite ou arquivo
|
|
33
|
+
que faltou: o canal aparece como "Não está pronto" e a crew pergunta se você quer corrigir agora
|
|
34
|
+
ou seguir assim. Se a legenda, o post ou o tweet passar do limite por causa das hashtags no fim,
|
|
35
|
+
o LEIA-ME traz um alerta.
|
|
36
|
+
- **Arquivo sem canal não se perde.** Proposta, minuta, relatório ou formato que não é de rede
|
|
37
|
+
nenhuma vai inteiro para `outros/`, com o nome original, e aparece no LEIA-ME.
|
|
38
|
+
- **Entrega de uma execução antiga.** Peça à IA para montar a entrega de uma execução já
|
|
39
|
+
encerrada: ela lista os arquivos, pede o seu "sim" e monta a pasta.
|
|
40
|
+
- Crew que publica sozinha (Instagram, por exemplo): a entrega é montada antes da publicação, e
|
|
41
|
+
o LEIA-ME avisa "Esta crew publica este canal sozinha. Antes de postar à mão, confira se já saiu."
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- **O fim da execução aponta para `entrega/` e para o LEIA-ME**, não mais para a pasta da execução
|
|
45
|
+
e para um "arquivo final" que ninguém dizia qual era. A cópia do arquivo final na raiz da
|
|
46
|
+
execução deixa de ser feita.
|
|
47
|
+
- A pasta `entrega/` é refeita do zero a cada entrega e fica fora do git: o que você editar ali se
|
|
48
|
+
perde. Para guardar, copie a pasta para outro lugar do projeto (o LEIA-ME avisa).
|
|
49
|
+
- Se o script da entrega não rodar (sem Node, por exemplo), a execução não para: a crew avisa e
|
|
50
|
+
lista os arquivos aprovados.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- **O título do arquivo era lido como legenda.** Num arquivo com `# Legenda — …` no topo e a
|
|
54
|
+
legenda de verdade mais abaixo, o verificador media duas legendas e podia dar alerta falso.
|
|
55
|
+
Agora o título do arquivo não conta como peça.
|
|
56
|
+
|
|
57
|
+
### Internal
|
|
58
|
+
- `_opencrew/core/scripts/entregar.mjs` e os módulos de `scripts/entrega/` (sem dependência, até
|
|
59
|
+
200 linhas cada); `_opencrew/core/prompts/entrega.prompt.md`; seção `### Entrega` no runner, que
|
|
60
|
+
perde "Save final output", "Run folder" e "Output saved to".
|
|
61
|
+
- Regra 15 do `AGENTS.md`: script do runtime só escreve onde foi combinado.
|
|
62
|
+
- Travas novas: `tests/entregar*.test.js`, `tests/runtime-contracts-u3a.test.js`,
|
|
63
|
+
`tests/upgrade-u3a.test.js` e o U3a-14c em `tests/package.test.js`.
|
|
64
|
+
- Roteiro renumerado: U3a fatia 2 = 1.9.0, U3b = 1.10.0, U4 = 1.11.0.
|
|
65
|
+
|
|
6
66
|
## [1.7.1] — 2026-10-06
|
|
7
67
|
|
|
8
68
|
Fase R3 "Reparos do runner em uso real" (`specs/fase-r3-runner-em-uso-real.md`): dois defeitos
|
package/README.md
CHANGED
|
@@ -32,6 +32,9 @@ dentro da sua IDE.**
|
|
|
32
32
|
mensal, lançamento de produto. Comece em 2 minutos.
|
|
33
33
|
- 📤 **Exportação multi-formato** — PDF, CSV e posts formatados por plataforma,
|
|
34
34
|
sem abrir editor nenhum.
|
|
35
|
+
- 📬 **Entrega por canal** — depois de aprovar, você encontra a pasta `entrega/`: uma pasta por
|
|
36
|
+
canal (Instagram, LinkedIn, blog, e-mail, WhatsApp, X/Twitter, YouTube), o texto pronto para
|
|
37
|
+
colar, as imagens e um `LEIA-ME.md` com o passo a passo. O que não está pronto fica marcado.
|
|
35
38
|
- 🎛️ **Seleção inteligente de agentes** — o sistema analisa seu pedido e
|
|
36
39
|
sugere quais agentes são necessários para aquela tarefa. Você confirma ou
|
|
37
40
|
ajusta com um clique. Agentes pulados não gastam tokens naquele run.
|
|
@@ -178,9 +181,9 @@ meu-projeto/
|
|
|
178
181
|
│ │ ├── skills.engine.md ← gerenciador de skills
|
|
179
182
|
│ │ ├── architect.agent.yaml ← definição do Arquiteto
|
|
180
183
|
│ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
|
|
181
|
-
│ │ ├── scripts/ ← verificador, conferência de fontes e os scripts do Escritório
|
|
184
|
+
│ │ ├── scripts/ ← verificador, conferência de fontes, caminhos, entrega e os scripts do Escritório
|
|
182
185
|
│ │ ├── escritorio/ ← página do Escritório ao vivo (abre com /opencrew dashboard)
|
|
183
|
-
│ │ └── prompts/ ←
|
|
186
|
+
│ │ └── prompts/ ← 14 prompts de fase (discovery, design, build, entrega, etc.)
|
|
184
187
|
│ ├── agents/ ← 5 agentes base compartilhados
|
|
185
188
|
│ │ ├── researcher.agent.md
|
|
186
189
|
│ │ ├── copywriter.agent.md
|
|
@@ -194,6 +197,9 @@ meu-projeto/
|
|
|
194
197
|
│
|
|
195
198
|
├── crews/ ← suas crews vivem aqui
|
|
196
199
|
│ ├── blog-semanal/ ← template: blog semanal
|
|
200
|
+
│ │ └── output/<execução>/ ← criada a cada execução
|
|
201
|
+
│ │ ├── v1/ v2/ … ← o que cada passo gravou
|
|
202
|
+
│ │ └── entrega/ ← o que você usa: LEIA-ME.md + uma pasta por canal
|
|
197
203
|
│ ├── instagram-carrossel/ ← template: Instagram carrossel
|
|
198
204
|
│ ├── newsletter-mensal/ ← template: newsletter
|
|
199
205
|
│ └── lancamento-produto/ ← template: lançamento
|
|
@@ -209,6 +215,50 @@ meu-projeto/
|
|
|
209
215
|
|
|
210
216
|
---
|
|
211
217
|
|
|
218
|
+
## Entrega por canal
|
|
219
|
+
|
|
220
|
+
Depois da aprovação final, a crew monta a pasta `entrega/` dentro da pasta da execução
|
|
221
|
+
(`crews/<crew>/output/<execução>/entrega/`). É ali que está o que você vai usar:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
entrega/
|
|
225
|
+
├── LEIA-ME.md ← comece por aqui: o que fazer com cada arquivo, canal por canal
|
|
226
|
+
├── instagram/ ← legenda.txt (hashtags no fim) e as imagens, com o nome original
|
|
227
|
+
├── linkedin/ ← post.txt e, se houver, post-comentario.txt (o primeiro comentário)
|
|
228
|
+
├── blog/ ← seo.txt (título, meta description, palavra-chave, slug) e artigo.md
|
|
229
|
+
├── email/ ← assunto.txt, previa.txt e corpo.md
|
|
230
|
+
├── whatsapp/ ← mensagem.txt
|
|
231
|
+
├── twitter/ ← tweet.txt (na thread: tweet-1.txt, tweet-2.txt…)
|
|
232
|
+
├── youtube/ ← o roteiro
|
|
233
|
+
├── outros/ ← arquivo sem canal (proposta, minuta, relatório), como está
|
|
234
|
+
└── editaveis/ ← o HTML dos slides, para quem quiser ajustar
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Só aparecem as pastas que a execução tem.
|
|
238
|
+
|
|
239
|
+
- **Texto pronto para colar.** Os `.txt` saem sem `#`, `**`, rótulos nem recados internos, com as
|
|
240
|
+
hashtags no fim. Com mais de uma peça do mesmo tipo, os arquivos são numerados (`post-1.txt`,
|
|
241
|
+
`post-2.txt`).
|
|
242
|
+
- **O `LEIA-ME.md` diz o que fazer.** Cada canal tem a situação ("Pronto" ou "Não está pronto"),
|
|
243
|
+
os arquivos, de onde cada um veio e os passos, numerados. "Antes de usar" junta o que falta;
|
|
244
|
+
"O que não foi conferido" lembra o que ninguém mediu (links e fatos, texto dentro das imagens,
|
|
245
|
+
aparência final em cada rede).
|
|
246
|
+
- **O que não está pronto fica marcado.** Sobrou um `[PREENCHER]` ou um texto acima do limite? O
|
|
247
|
+
canal aparece como "Não está pronto" e a crew pergunta se você quer corrigir agora ou seguir
|
|
248
|
+
assim.
|
|
249
|
+
- **Arquivo sem canal vai para `outros/`**, inteiro e com o nome original: nada some.
|
|
250
|
+
- **A pasta é refeita a cada entrega e fica fora do git.** O que você editar ali se perde; para
|
|
251
|
+
guardar, copie a pasta para outro lugar do projeto.
|
|
252
|
+
- **Execução antiga?** Peça à IA: "monte a entrega da execução X da crew Y". Ela lista os
|
|
253
|
+
arquivos, pede o seu "sim" e monta a pasta. Funciona em crews criadas antes da 1.8.0, depois
|
|
254
|
+
do `update`.
|
|
255
|
+
|
|
256
|
+
A entrega não gera PDF nem imagem: `artigo.md`, `corpo.md` e os roteiros saem em markdown, e o
|
|
257
|
+
LEIA-ME ensina a salvar como PDF pelo "Imprimir" do seu editor. O LEIA-ME e os nomes dos arquivos
|
|
258
|
+
são sempre em português.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
212
262
|
## Escritório ao vivo
|
|
213
263
|
|
|
214
264
|
Quer ver a equipe trabalhando? O **Escritório** é uma página em pixel-art, aberta no navegador,
|
package/package.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -61,6 +61,7 @@ Route input to the matching action:
|
|
|
61
61
|
| `/opencrew dashboard` | Turn on and open the Escritório (live view) — see "Dashboard (Optional)" |
|
|
62
62
|
| `/opencrew dashboard off` | Turn the Escritório off — see "Dashboard (Optional)" |
|
|
63
63
|
| `/opencrew reset` | Confirm and reset all configuration |
|
|
64
|
+
| Request to deliver a run that already ended ("monte a entrega da execução …") | Load `_opencrew/core/prompts/entrega.prompt.md` → build the `entrega/` folder of that run |
|
|
64
65
|
| Natural language about crews | Infer intent and route accordingly |
|
|
65
66
|
|
|
66
67
|
## Loading Agents
|
|
@@ -120,6 +121,8 @@ and touch nothing else: stop no process, delete no file.
|
|
|
120
121
|
- Exception: crew memory scaffolding (`memories.md` headers, `runs.md` columns)
|
|
121
122
|
keeps fixed PT-BR structural labels regardless of the user's language —
|
|
122
123
|
see `_opencrew/core/runner.pipeline.md`
|
|
124
|
+
- Exception: the delivery folder (`entrega/`) — its folder names, file names and the `LEIA-ME.md`
|
|
125
|
+
are written by a script in fixed PT-BR, whatever the user's language
|
|
123
126
|
|
|
124
127
|
## Critical Rules
|
|
125
128
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.8.0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Entrega — The Delivery Folder of a Run
|
|
2
|
+
|
|
3
|
+
One script turns the approved files of a run into `crews/{name}/output/{run_id}/entrega/`: one
|
|
4
|
+
folder per channel, text ready to paste and a `LEIA-ME.md` that tells the user what to do with each
|
|
5
|
+
file. Your part is to build the list of files, run the script and act on its last line.
|
|
6
|
+
|
|
7
|
+
You do NOT copy, split, rename or rewrite a file yourself, and you never write inside `entrega/`:
|
|
8
|
+
the script rebuilds that folder from scratch on every call. **When** the delivery runs is in the
|
|
9
|
+
section "Entrega" of `_opencrew/core/runner.pipeline.md`; a request to deliver a run that is
|
|
10
|
+
already over starts at "A run that already ended", below.
|
|
11
|
+
|
|
12
|
+
## Step 1: Build the list
|
|
13
|
+
|
|
14
|
+
`{lista}` is one item per file, separated by commas, each written `{caminho}={formato}`.
|
|
15
|
+
|
|
16
|
+
- **What goes in:** for each **creation or rendering step** of the approved run, every path the
|
|
17
|
+
`saida` action of `caminho.mjs` returned the **last time** the step ran (a step sent back by the
|
|
18
|
+
reviewer ran more than once: only its last version counts) — all the output files of the step,
|
|
19
|
+
images and HTML included. Use the paths you stored during the run. Never look for a `vN` folder
|
|
20
|
+
by yourself and never assume `v1`.
|
|
21
|
+
- **`{formato}`** is the `format:` of the step that wrote the file. A rendering step with no
|
|
22
|
+
`format:` uses the `format:` of the content step it renders (the slides of an `instagram-feed`
|
|
23
|
+
carousel go as `=instagram-feed`). With no format either way, with an export format (`pdf`,
|
|
24
|
+
`csv`, `formatted-post`) or with one outside `[a-z0-9-]+`, the item goes without `={formato}`:
|
|
25
|
+
the script puts that file in `outros/`.
|
|
26
|
+
This list is **not** the list of the checker (`verificar.mjs`), where a step with no `format:`
|
|
27
|
+
goes without one: here the rendering step (images, HTML) takes the format of the content step it
|
|
28
|
+
renders — the step that wrote its `inputFile`. Without it the images land in `outros/`.
|
|
29
|
+
- **Stay out:** research, briefing, the reviewer's verdict, checkpoint answers and every skipped
|
|
30
|
+
step (a deselected agent, or a step the user skipped).
|
|
31
|
+
- **Safe names** — the safe-name rule (nome seguro) of the runner applies. A file that rule left
|
|
32
|
+
out of commands stays out of the list too; tell the user:
|
|
33
|
+
`{arquivo} — ficou fora da entrega: nome com caractere que não vai em comando`.
|
|
34
|
+
|
|
35
|
+
## Step 2: Channels the crew publishes by itself
|
|
36
|
+
|
|
37
|
+
Add `--vai-publicar {canal}` once for each channel of the list that has an irreversible step in the
|
|
38
|
+
pipeline (`side_effects: irreversible`, in the step or in the agent's skill), whether that step
|
|
39
|
+
already ran or not. The `LEIA-ME.md` then opens that channel with: "Esta crew publica este canal
|
|
40
|
+
sozinha. Antes de postar à mão, confira se já saiu."
|
|
41
|
+
|
|
42
|
+
- The channel of a step is the `platform:` of its `format:` — read it in
|
|
43
|
+
`_opencrew/best-practices.local/{format}.md` when that file declares `platform:`, otherwise in
|
|
44
|
+
`_opencrew/core/best-practices/{format}.md`. For a step with no `format:`, the one of the skill
|
|
45
|
+
(`instagram-publisher` → `instagram`). With neither, do not pass the option.
|
|
46
|
+
- `{canal}` is the folder name: `instagram`, `linkedin`, `blog`, `email`, `whatsapp`, `twitter` or
|
|
47
|
+
`youtube`. Only a channel that has an item in the list (an item whose format has that
|
|
48
|
+
`platform:`) — for any other the script stops with `Canal não encontrado nesta entrega: {canal}.`
|
|
49
|
+
|
|
50
|
+
## Step 3: Run the script
|
|
51
|
+
|
|
52
|
+
From the project root, one line, everything between double quotes:
|
|
53
|
+
|
|
54
|
+
`node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}"`
|
|
55
|
+
|
|
56
|
+
With channels from Step 2, the same command ends with one option per channel:
|
|
57
|
+
|
|
58
|
+
`node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}" --vai-publicar {canal}`
|
|
59
|
+
|
|
60
|
+
## Step 4: Show the result and read the last line
|
|
61
|
+
|
|
62
|
+
The output of the script is the final summary of the run (the folder, each channel as "Pronto" or
|
|
63
|
+
"Não está pronto", what is missing, the path of the `LEIA-ME.md`): show it to the user as it came,
|
|
64
|
+
without the `ENTREGA:` line. Do not rewrite it and do not add files it does not list. Besides the
|
|
65
|
+
`entrega/` folder, the script also writes `crews/{name}/output/{run_id}/verificacao-entrega.md`,
|
|
66
|
+
the report of the check made at delivery time (it is not part of the delivery).
|
|
67
|
+
|
|
68
|
+
- `ENTREGA:OK` → go on with the run.
|
|
69
|
+
- `ENTREGA:INCOMPLETA` → a channel is not ready, or a file could not be written. The files were
|
|
70
|
+
generated anyway and the `LEIA-ME.md` marks the channel. Show what is missing and ask:
|
|
71
|
+
```
|
|
72
|
+
⚠️ A entrega ficou incompleta: {o que falta}
|
|
73
|
+
1. Corrigir agora (eu ajusto e monto a entrega de novo)
|
|
74
|
+
2. Seguir assim (no LEIA-ME, o canal fica marcado como "Não está pronto")
|
|
75
|
+
```
|
|
76
|
+
Wait for the answer.
|
|
77
|
+
- **1** — for each item that is missing, fix it in the source file the summary names: ask the
|
|
78
|
+
user for the real information of every `[PREENCHER: …]`, shorten what is over a limit, write
|
|
79
|
+
again a file that is not there (when the summary says a file could not be written, there is
|
|
80
|
+
nothing to fix in the text). Then run the delivery again, with the same list.
|
|
81
|
+
- **2** — go on, with the delivery as it is. Every irreversible step still asks for its own
|
|
82
|
+
confirmation, as it does today.
|
|
83
|
+
|
|
84
|
+
After "Edit this content" (the final menu of the runner) changes an approved file, or any step
|
|
85
|
+
runs again after the delivery, run the delivery again with the new paths: the folder is rebuilt
|
|
86
|
+
from scratch, so whatever was edited inside `entrega/` is lost — the `LEIA-ME.md` says so.
|
|
87
|
+
|
|
88
|
+
## When the script does not run
|
|
89
|
+
|
|
90
|
+
The script did not run when there is no Node, an error, or no `ENTREGA:` line at the end (it prints
|
|
91
|
+
one line in PT-BR and stops, with nothing written). Then never build the folder by hand. If the line points to something in the
|
|
92
|
+
command you wrote (an option, a channel, the run), fix the command and run it once more. Otherwise
|
|
93
|
+
tell the user, list the approved files (the paths of your list) and go on with the run:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
⚠️ A entrega automática não rodou: {motivo}
|
|
97
|
+
Os arquivos aprovados estão em:
|
|
98
|
+
- {caminho}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`{motivo}` is the line the script printed, or what kept it from running. The completion summary
|
|
102
|
+
of the runner then shows this list in place of the `entrega/` folder.
|
|
103
|
+
|
|
104
|
+
## A run that already ended
|
|
105
|
+
|
|
106
|
+
When the user asks to deliver a run that is over (it was run before this folder existed, or the
|
|
107
|
+
delivery did not run):
|
|
108
|
+
|
|
109
|
+
1. Find the crew and the run. If the user did not say which, list the folders of
|
|
110
|
+
`crews/{name}/output/` with the IDE's folder-listing tool (no shell command) and ask.
|
|
111
|
+
2. Read the steps of `crews/{name}/pipeline/`. For the `outputFile` of each creation or rendering
|
|
112
|
+
step (same rules of Step 1 for what stays out and for `{formato}`), run:
|
|
113
|
+
`node _opencrew/core/scripts/caminho.mjs "{name}" entrada --run "{run_id}" --arquivo "{outputFile}"`
|
|
114
|
+
`CAMINHO:OK {path}` → that path is the item. `CAMINHO:FALTA` → the file is not in that run:
|
|
115
|
+
leave it out.
|
|
116
|
+
3. Nobody recorded what was approved in that run, so show the list and wait for the "sim":
|
|
117
|
+
```
|
|
118
|
+
Entrega da execução {run_id} da crew {name}. Arquivos:
|
|
119
|
+
- {caminho} ({formato})
|
|
120
|
+
Posso montar a entrega com esta lista? (sim / não)
|
|
121
|
+
```
|
|
122
|
+
4. On "sim", follow Steps 2 to 4. No step of the pipeline runs again, and nothing is published.
|
|
123
|
+
|
|
124
|
+
## Rules
|
|
125
|
+
|
|
126
|
+
- **DO** show the output of the script as it came; the folder and file names and the `LEIA-ME.md`
|
|
127
|
+
are fixed PT-BR, whatever the user's language.
|
|
128
|
+
- **DO** run the delivery again whenever an approved file changes.
|
|
129
|
+
- **DO NOT** create, edit or delete anything inside `entrega/` yourself.
|
|
130
|
+
- **DO NOT** put in the list a file the user did not approve, nor a path you guessed.
|
|
131
|
+
- **DO NOT** treat `ENTREGA:INCOMPLETA` as an error of the script: it is its answer.
|
|
@@ -500,6 +500,7 @@ root the one-line command of each moment and read the last line (`CAMINHO:OK {pa
|
|
|
500
500
|
is empty → this check never fires (legacy behavior).
|
|
501
501
|
|
|
502
502
|
0b. **Escritório** — if it is on, run `passo`, or `checkpoint` when the step is a checkpoint (see "Escritório" above).
|
|
503
|
+
0c. **Entrega** — before the first step that publishes or sends, once the final approval was given, run the delivery (see "Entrega" below).
|
|
503
504
|
|
|
504
505
|
1. **Pre-Step Input Validation** — MANDATORY. If the step's frontmatter declares an `inputFile`, the input comes from the `entrada` action, never from a path you build: validate that the input exists before executing the step. Run the `entrada` command (Output Path Transformation) with the `inputFile` as declared:
|
|
505
506
|
- `CAMINHO:OK {path}` → that path is the step's input (the newest version that has the file): read the input from it and execute the step.
|
|
@@ -715,6 +716,7 @@ For reference, the complete execution order for each pipeline step is:
|
|
|
715
716
|
```
|
|
716
717
|
0. Agent deselection check (skip step if its agent was deselected)
|
|
717
718
|
0b. Escritório command (passo or checkpoint) — only if it is on
|
|
719
|
+
0c. Entrega (delivery script) — only before the first step that publishes or sends
|
|
718
720
|
1. Pre-Step Input Validation (script gate: `entrada`)
|
|
719
721
|
2. Read step file
|
|
720
722
|
3. Check execution mode and execute (subagent / inline / checkpoint)
|
|
@@ -724,10 +726,18 @@ For reference, the complete execution order for each pipeline step is:
|
|
|
724
726
|
|
|
725
727
|
Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT advance — the user is consulted.
|
|
726
728
|
|
|
729
|
+
### Entrega
|
|
730
|
+
|
|
731
|
+
One script turns the approved files into `crews/{name}/output/{run_id}/entrega/` (a folder per channel, text ready to paste, a `LEIA-ME.md`). Read `_opencrew/core/prompts/entrega.prompt.md` and follow it: how to build `{lista}`, when to add `--vai-publicar`, what to do with `ENTREGA:OK`, with `ENTREGA:INCOMPLETA` and with a script that did not run.
|
|
732
|
+
|
|
733
|
+
- **Command** — from the project root, by the safe-name rule (nome seguro), everything between double quotes: `node _opencrew/core/scripts/entregar.mjs --crew "crews/{name}" --run "{run_id}" --arquivo "{lista}"`
|
|
734
|
+
- **When** — once, after the final approval, immediately before the first step that publishes or sends (`side_effects: irreversible`, in the step or in the agent's skill); with no such step, after the last step. Always before the end-of-run command of the Escritório. If the irreversible step comes before the final approval (a crew built by an old version), the delivery runs at the end.
|
|
735
|
+
- **Crew with no final approval checkpoint** — same moments, and show `Esta crew não tem aprovação final: confira os arquivos antes de usar.` **Never** for a run that was rejected, aborted before the final approval or left with no approved file.
|
|
736
|
+
- The output of the script is the final summary of the run: show it as it came; if the run stops later, at an irreversible step, show it before stopping. After "Edit this content" changes an approved file, run it again.
|
|
737
|
+
|
|
727
738
|
### After Pipeline Completion
|
|
728
739
|
|
|
729
|
-
1.
|
|
730
|
-
(The run folder was created during initialization — no separate date subfolder needed)
|
|
740
|
+
1. **Entrega** — if the delivery has not run in this run, run it now (see "Entrega" above).
|
|
731
741
|
1b. **Escritório** — if it is on, run `concluir` (see "Escritório" above).
|
|
732
742
|
|
|
733
743
|
2. **Update crew memory** — write to BOTH files:
|
|
@@ -828,8 +838,7 @@ Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT ad
|
|
|
828
838
|
```
|
|
829
839
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
830
840
|
✅ Pipeline complete!
|
|
831
|
-
📁
|
|
832
|
-
📄 Output saved to: {output path}
|
|
841
|
+
📁 Delivery: crews/{name}/output/{run_id}/entrega/ — start with LEIA-ME.md
|
|
833
842
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
834
843
|
|
|
835
844
|
What would you like to do?
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Linha de comando do `entregar.mjs`: as opções, a lista `caminho=formato` e a linha de uso.
|
|
2
|
+
// Spec: fase-u3a1-pasta-de-entrega.md, §3 e §6 (repositório do OpenCrew).
|
|
3
|
+
import { lerItemDaLista } from '../verificar/argumentos.mjs';
|
|
4
|
+
|
|
5
|
+
export const USO = 'Uso: node _opencrew/core/scripts/entregar.mjs --crew "crews/<crew>" --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--vai-publicar <canal>] [--ajuda]';
|
|
6
|
+
|
|
7
|
+
const OPCAO = /^--(crew|run|arquivo|vai-publicar)(?:=(.*))?$/s;
|
|
8
|
+
|
|
9
|
+
/** Texto que veio da linha de comando e volta numa mensagem: uma linha só, até 200 caracteres. */
|
|
10
|
+
export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
11
|
+
|
|
12
|
+
function guardar(args, nome, valor) {
|
|
13
|
+
if (nome === 'arquivo') args.arquivos.push(valor);
|
|
14
|
+
else if (nome === 'vai-publicar') args.vaiPublicar.push(valor.trim().toLowerCase());
|
|
15
|
+
else args[nome] = valor;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `argv` → `{ crew, run, arquivos, vaiPublicar, ajuda, desconhecida }`. Opção vale como
|
|
20
|
+
* `--nome valor` e `--nome=valor`. `--arquivo` e `--vai-publicar` repetidos somam, e o que vem
|
|
21
|
+
* solto logo depois da lista é mais um item dela. `desconhecida`: a primeira opção que o script
|
|
22
|
+
* não conhece, ou null.
|
|
23
|
+
*/
|
|
24
|
+
export function lerArgs(argv) {
|
|
25
|
+
const args = { arquivos: [], vaiPublicar: [], ajuda: false, desconhecida: null };
|
|
26
|
+
let naLista = false;
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const [, nome, colado] = argv[i].match(OPCAO) ?? [];
|
|
29
|
+
const solto = !nome && !argv[i].startsWith('--');
|
|
30
|
+
if (nome) guardar(args, nome, colado ?? (i + 1 < argv.length && !argv[i + 1].startsWith('--') ? argv[++i] : ''));
|
|
31
|
+
else if (argv[i] === '--ajuda') args.ajuda = true;
|
|
32
|
+
else if (solto && naLista) args.arquivos.push(argv[i]);
|
|
33
|
+
else args.desconhecida ??= argv[i];
|
|
34
|
+
naLista = nome ? nome === 'arquivo' : solto && naLista;
|
|
35
|
+
}
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A lista de `--arquivo` → `[{ arquivo, formato }]` (`formato` null quando não veio `=formato`). */
|
|
40
|
+
export function lerLista(arquivos) {
|
|
41
|
+
const textos = arquivos.join(',').split(',').map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
return textos.map(lerItemDaLista).map((i) => (typeof i === 'string' ? { arquivo: i, formato: null } : i));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** O `--run` é um segmento só: sem `/`, `\` nem `..`. */
|
|
46
|
+
export const runValido = (run) => !/[\\/]/.test(run) && run !== '..' && run !== '.';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Canais da entrega: a pasta de cada formato (o `platform:` do best-practice), os nomes que o
|
|
2
|
+
// LEIA-ME mostra e o que nunca entra numa entrega.
|
|
3
|
+
// Spec: fase-u3a1-pasta-de-entrega.md, regras 1 e 3 (repositório do OpenCrew).
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { lerFrontmatter, lerTexto } from '../verificar/leitura.mjs';
|
|
7
|
+
|
|
8
|
+
/** Pasta do canal → nome no LEIA-ME, na ordem em que as seções aparecem. */
|
|
9
|
+
export const CANAIS = { instagram: 'Instagram', linkedin: 'LinkedIn', blog: 'Blog', email: 'E-mail', whatsapp: 'WhatsApp', twitter: 'X/Twitter', youtube: 'YouTube' };
|
|
10
|
+
export const OUTROS = 'outros';
|
|
11
|
+
export const EDITAVEIS = 'editaveis';
|
|
12
|
+
export const ehCanal = (pasta) => Object.hasOwn(CANAIS, pasta);
|
|
13
|
+
/** Como a pasta é chamada numa mensagem: o nome do canal, ou `outros/`. */
|
|
14
|
+
export const nomeDaPasta = (pasta) => CANAIS[pasta] ?? `${pasta}/`;
|
|
15
|
+
|
|
16
|
+
async function plataformaEm(raiz, pasta, formato) {
|
|
17
|
+
const arquivo = path.join(raiz, '_opencrew', ...pasta, `${formato}.md`);
|
|
18
|
+
if (!existsSync(arquivo)) return null;
|
|
19
|
+
const valor = lerFrontmatter(await lerTexto(arquivo))?.platform;
|
|
20
|
+
return typeof valor === 'string' && valor.trim() ? valor.trim().toLowerCase() : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Canal de um formato: o `platform:` de `_opencrew/best-practices.local/<formato>.md`, quando o
|
|
25
|
+
* arquivo o declara; senão, o do core. Sem formato, sem best-practice, sem `platform:` ou com
|
|
26
|
+
* plataforma que não é uma das sete pastas: null (o arquivo vai para `outros/`).
|
|
27
|
+
*/
|
|
28
|
+
export async function canalDoFormato(raiz, formato) {
|
|
29
|
+
if (!formato || !/^[a-z0-9-]+$/.test(formato)) return null;
|
|
30
|
+
const local = await plataformaEm(raiz, ['best-practices.local'], formato);
|
|
31
|
+
const plataforma = local ?? (await plataformaEm(raiz, ['core', 'best-practices'], formato));
|
|
32
|
+
return plataforma && ehCanal(plataforma) ? plataforma : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PASTAS_DE_SERVICO = new Set(['entrega', 'entrega.tmp', 'export']);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Arquivo de serviço nunca entra, mesmo listado: `verificacao-*.md`, `publicado.json`, o que está
|
|
39
|
+
* em `entrega/`, `entrega.tmp/` ou `export/` de uma execução e o `caption.txt` direto na pasta
|
|
40
|
+
* da execução (é ali que o publicador o grava; dentro de uma pasta `vN` ele é saída de passo).
|
|
41
|
+
* @param {string} rel caminho relativo ao projeto, com `/`
|
|
42
|
+
*/
|
|
43
|
+
export function ehDeServico(rel) {
|
|
44
|
+
const partes = rel.toLowerCase().split('/');
|
|
45
|
+
const nome = partes.at(-1);
|
|
46
|
+
if (/^verificacao-.*\.md$/.test(nome) || nome === 'publicado.json') return true;
|
|
47
|
+
const naSaida = partes[0] === 'crews' && partes[2] === 'output';
|
|
48
|
+
if (!naSaida) return false;
|
|
49
|
+
if (partes.slice(4, -1).some((p) => PASTAS_DE_SERVICO.has(p))) return true;
|
|
50
|
+
return nome === 'caption.txt' && partes.length === 5;
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Blocos de rótulo do arquivo de origem que não chegam à entrega: os de serviço (notas, checklist,
|
|
2
|
+
// FORMAT) e, nos formatos lidos pelo leitor de peças, os que não são a peça do formato (os SLIDES
|
|
3
|
+
// de um carrossel, por exemplo). A entrega não muda: o LEIA-ME só passa a dizer o que ficou fora.
|
|
4
|
+
// Spec: fase-u3a1-pasta-de-entrega.md, §4 e §6, ajuste da execução real (repositório do OpenCrew).
|
|
5
|
+
import { semFrontmatter } from '../verificar/leitura.mjs';
|
|
6
|
+
import { ROTULOS } from '../verificar/pecas.mjs';
|
|
7
|
+
import { lerTrechos, marcar } from '../verificar/secoes.mjs';
|
|
8
|
+
import { PRINCIPAL } from './leitor.mjs';
|
|
9
|
+
import { ehServico, secoesDeRotulo, semComentarios } from './texto.mjs';
|
|
10
|
+
|
|
11
|
+
export const MSG = {
|
|
12
|
+
fora: (blocos) => `Ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem.`,
|
|
13
|
+
foraNaTela: (arquivo, blocos) => `${arquivo}: ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem.`,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const tabelaDe = (formato) => (formato === 'twitter-thread' ? 'twitter-post' : formato);
|
|
17
|
+
|
|
18
|
+
/** O bloco deste rótulo chega à entrega? `formato`: o do leitor de peças, ou null (só o de serviço sai). */
|
|
19
|
+
function entra(rotulo, formato) {
|
|
20
|
+
if (ehServico(rotulo)) return false;
|
|
21
|
+
if (!formato) return true;
|
|
22
|
+
const papel = ROTULOS[tabelaDe(formato)]?.[rotulo];
|
|
23
|
+
return papel === PRINCIPAL[formato] || papel === 'parte' || rotulo === 'HASHTAGS';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** As seções de rótulo como o leitor de peças as vê: cada uma vai até o próximo rótulo ou cabeçalho de peça. */
|
|
27
|
+
function secoesDoLeitor(corpo, formato) {
|
|
28
|
+
const trechos = lerTrechos(marcar(corpo), tabelaDe(formato)).filter((t) => t.rotulo);
|
|
29
|
+
return trechos.map((t) => ({ rotulo: t.rotulo, texto: t.linhas.join('\n').trim() }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Rótulos dos blocos com texto que ficam fora da entrega, cada um uma vez, na ordem do arquivo.
|
|
34
|
+
* @param {string} texto o arquivo de origem, sem BOM e com LF
|
|
35
|
+
* @param {string|null} [formato] formato lido pelo leitor de peças (`instagram-feed`,
|
|
36
|
+
* `linkedin-post`, `twitter-post`, `twitter-thread`); null nos outros, em que só o bloco de
|
|
37
|
+
* serviço fica fora
|
|
38
|
+
* @returns {string[]}
|
|
39
|
+
*/
|
|
40
|
+
export function blocosFora(texto, formato = null) {
|
|
41
|
+
const corpo = semComentarios(semFrontmatter(texto));
|
|
42
|
+
const secoes = formato ? secoesDoLeitor(corpo, formato) : secoesDeRotulo(corpo);
|
|
43
|
+
return [...new Set(secoes.filter((s) => s.rotulo && s.texto && !entra(s.rotulo, formato)).map((s) => s.rotulo))];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** O aviso de um arquivo: `{ pasta, texto, tela }`, ou nenhum quando nada ficou fora. */
|
|
47
|
+
export function avisoDeFora(item, blocos) {
|
|
48
|
+
return blocos.length ? [{ pasta: item.canal, texto: MSG.fora(blocos), tela: MSG.foraNaTela(item.rel, blocos) }] : [];
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Gravação da entrega: montada em `entrega.tmp/`, ao lado, e trocada no fim. Nada pela metade:
|
|
2
|
+
// em qualquer falha a entrega anterior fica como estava e não sobra pasta temporária. O script só
|
|
3
|
+
// apaga a própria `entrega/` e os temporários que ele mesmo criou.
|
|
4
|
+
// Spec: fase-u3a1-pasta-de-entrega.md, regras 12 e 15 (repositório do OpenCrew).
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const apagar = (pasta) => fs.rm(pasta, { recursive: true, force: true, maxRetries: 3 });
|
|
9
|
+
|
|
10
|
+
/** 'pasta', 'arquivo' ou null (não existe). */
|
|
11
|
+
async function tipoDe(caminho) {
|
|
12
|
+
try {
|
|
13
|
+
return (await fs.stat(caminho)).isDirectory() ? 'pasta' : 'arquivo';
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Temporário que sobrou de uma chamada interrompida: a pasta é do script; arquivo com o mesmo nome não é. */
|
|
20
|
+
async function limparSobras({ entrega, tmp, antiga }) {
|
|
21
|
+
if ((await tipoDe(antiga)) === 'pasta') {
|
|
22
|
+
if (await tipoDe(entrega)) await apagar(antiga);
|
|
23
|
+
else await fs.rename(antiga, entrega); // a troca anterior parou no meio: a entrega volta
|
|
24
|
+
}
|
|
25
|
+
if ((await tipoDe(tmp)) === 'pasta') await apagar(tmp);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Grava um arquivo da entrega na pasta temporária: o texto gerado, ou os mesmos bytes da origem. */
|
|
29
|
+
async function gravarArquivo(destino, a) {
|
|
30
|
+
await fs.mkdir(path.dirname(destino), { recursive: true });
|
|
31
|
+
if (a.texto != null) await fs.writeFile(destino, a.texto, 'utf8');
|
|
32
|
+
else await fs.copyFile(a.de, destino);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Põe a pasta nova no lugar da anterior; se a nova não entra, a anterior volta. */
|
|
36
|
+
async function trocar({ entrega, tmp, antiga }, passo) {
|
|
37
|
+
const anterior = await tipoDe(entrega);
|
|
38
|
+
passo(entrega);
|
|
39
|
+
if (anterior === 'arquivo') throw new Error('entrega não é uma pasta');
|
|
40
|
+
if (anterior) await fs.rename(entrega, antiga);
|
|
41
|
+
try {
|
|
42
|
+
await fs.rename(tmp, entrega);
|
|
43
|
+
} catch (erro) {
|
|
44
|
+
if (anterior) await fs.rename(antiga, entrega);
|
|
45
|
+
throw erro;
|
|
46
|
+
}
|
|
47
|
+
passo(antiga);
|
|
48
|
+
if (anterior) await apagar(antiga);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Refaz `entrega/` do zero e grava, ao lado, o relatório da verificação.
|
|
53
|
+
* @param {string} execucao pasta da execução (`crews/<crew>/output/<run>/`), absoluta
|
|
54
|
+
* @param {object[]} arquivos `{ pasta, nome, texto | de }`
|
|
55
|
+
* @param {{ leiame: string, relatorio: string }} textos
|
|
56
|
+
* @returns {Promise<string|null>} null quando gravou tudo; senão, o caminho que não foi gravado
|
|
57
|
+
*/
|
|
58
|
+
export async function gravarEntrega(execucao, arquivos, { leiame, relatorio }) {
|
|
59
|
+
const pastas = { entrega: path.join(execucao, 'entrega'), tmp: path.join(execucao, 'entrega.tmp'), antiga: path.join(execucao, 'entrega.antiga.tmp') };
|
|
60
|
+
let alvo = pastas.tmp;
|
|
61
|
+
let criei = false;
|
|
62
|
+
const passo = (caminho) => { alvo = caminho; };
|
|
63
|
+
try {
|
|
64
|
+
await limparSobras(pastas);
|
|
65
|
+
await fs.mkdir(pastas.tmp);
|
|
66
|
+
criei = true;
|
|
67
|
+
for (const a of arquivos) {
|
|
68
|
+
passo(path.join(pastas.tmp, a.pasta, a.nome));
|
|
69
|
+
await gravarArquivo(alvo, a);
|
|
70
|
+
}
|
|
71
|
+
passo(path.join(pastas.tmp, 'LEIA-ME.md'));
|
|
72
|
+
await fs.writeFile(alvo, leiame, 'utf8');
|
|
73
|
+
await trocar(pastas, passo);
|
|
74
|
+
criei = false;
|
|
75
|
+
passo(path.join(execucao, 'verificacao-entrega.md'));
|
|
76
|
+
await fs.writeFile(alvo, relatorio, 'utf8');
|
|
77
|
+
return null;
|
|
78
|
+
} catch {
|
|
79
|
+
if (criei) await apagar(pastas.tmp).catch(() => {});
|
|
80
|
+
// O arquivo que falhou dentro da pasta temporária é citado pelo lugar em que ficaria.
|
|
81
|
+
return alvo.startsWith(pastas.tmp + path.sep) ? path.join(pastas.entrega, alvo.slice(pastas.tmp.length + 1)) : alvo;
|
|
82
|
+
}
|
|
83
|
+
}
|