@aksp/opencrew 1.8.0 → 1.9.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 +69 -0
- package/README.md +45 -12
- package/package.json +1 -1
- package/src/commands/update.js +9 -3
- package/src/lib/blocos.js +7 -3
- package/src/lib/resumo.js +3 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/discovery.prompt.md +1 -1
- package/templates/_opencrew/core/prompts/entrega.prompt.md +90 -16
- package/templates/_opencrew/core/prompts/export.prompt.md +5 -81
- package/templates/_opencrew/core/runner.pipeline.md +20 -20
- package/templates/_opencrew/core/scripts/entrega/argumentos.mjs +13 -9
- package/templates/_opencrew/core/scripts/entrega/canais.mjs +5 -3
- package/templates/_opencrew/core/scripts/entrega/comparar.mjs +104 -0
- package/templates/_opencrew/core/scripts/entrega/copia.mjs +138 -0
- package/templates/_opencrew/core/scripts/entrega/destino.mjs +98 -0
- package/templates/_opencrew/core/scripts/entrega/fora.mjs +4 -3
- package/templates/_opencrew/core/scripts/entrega/guardar.mjs +60 -0
- package/templates/_opencrew/core/scripts/entrega/leiame.mjs +52 -16
- package/templates/_opencrew/core/scripts/entrega/leitor.mjs +16 -3
- package/templates/_opencrew/core/scripts/entrega/lembrar.mjs +65 -0
- package/templates/_opencrew/core/scripts/entrega/passos.mjs +10 -5
- package/templates/_opencrew/core/scripts/entrega/pendencias.mjs +16 -6
- package/templates/_opencrew/core/scripts/entrega/ressalvas.mjs +78 -0
- package/templates/_opencrew/core/scripts/entrega/resumo.mjs +29 -0
- package/templates/_opencrew/core/scripts/entrega/retrato.mjs +42 -0
- package/templates/_opencrew/core/scripts/entrega/separar.mjs +6 -3
- package/templates/_opencrew/core/scripts/entregar.mjs +64 -39
- package/templates/_opencrew/core/scripts/verificar/argumentos.mjs +4 -4
- package/templates/_opencrew/core/scripts/verificar/entradas.mjs +26 -0
- package/templates/_opencrew/core/scripts/verificar/gravacao.mjs +41 -0
- package/templates/_opencrew/core/scripts/verificar/relatorio.mjs +10 -3
- package/templates/_opencrew/core/scripts/verificar.mjs +17 -27
- package/templates/gitignore +1 -0
|
@@ -283,19 +283,17 @@ Before executing any step that references an agent:
|
|
|
283
283
|
- Apply Voice Guidance (vocabulary always/never use, tone rules)
|
|
284
284
|
5. **Inject format context**: Check if the current step's frontmatter contains a `format:` field.
|
|
285
285
|
If present:
|
|
286
|
-
a. **Export
|
|
287
|
-
- Read `_opencrew/core/prompts/export.prompt.md`
|
|
288
|
-
|
|
289
|
-
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
transform the content, and write the output file in the target format.
|
|
298
|
-
- Skip the best-practices lookup below for export formats.
|
|
286
|
+
a. **Export format** — if format is `csv`:
|
|
287
|
+
- Read `_opencrew/core/prompts/export.prompt.md` and append its Markdown body to the agent's
|
|
288
|
+
context, before skill instructions, under the line `--- EXPORT FORMAT: csv ---`
|
|
289
|
+
- The agent must follow the export process — read the input file, transform the content,
|
|
290
|
+
and write the output file in the target format. Skip the best-practices lookup below.
|
|
291
|
+
a2. **`pdf` or `formatted-post`** (a step of an old crew) — neither is generated any more. Say
|
|
292
|
+
`O formato "{id}" não é mais gerado. O passo segue sem ele e grava o texto em markdown. Para ter um PDF, use Imprimir → Salvar como PDF.`
|
|
293
|
+
(`{id}` = the format) and run it as a common step, with no format injection. For `pdf`, the
|
|
294
|
+
agent writes markdown and the `outputFile` is used with the extension `.md`: that is the
|
|
295
|
+
path that goes to `caminho.mjs` (`saida`, `conferir`) and that the next steps read (their
|
|
296
|
+
`inputFile`, too) — no `.pdf` is created.
|
|
299
297
|
b. **Content formats** — otherwise, read `_opencrew/best-practices.local/{format}.md` (the user's
|
|
300
298
|
own version, never touched by `update`) if it exists, else `_opencrew/core/best-practices/{format}.md`
|
|
301
299
|
(e.g., `_opencrew/core/best-practices/instagram-feed.md`)
|
|
@@ -669,10 +667,10 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
669
667
|
the `format:` of the step that generated that file; a step with no `format:`, with an export
|
|
670
668
|
format (`pdf`, `csv`, `formatted-post`) or with one outside `[a-z0-9-]+` goes without `=formato`:
|
|
671
669
|
```bash
|
|
672
|
-
node _opencrew/core/scripts/verificar.mjs --crew "crews/{name}" --arquivo "{path1}={format1},{path2},…"
|
|
670
|
+
node _opencrew/core/scripts/verificar.mjs --crew "crews/{name}" --arquivo "{path1}={format1},{path2},…" --relatorio "crews/{name}/output/{run_id}/verificacao-ciclo-{N}.md"
|
|
673
671
|
```
|
|
674
|
-
|
|
675
|
-
into the reviewer's context as `--- VERIFICAÇÃO AUTOMÁTICA ---`. The reviewer must copy the
|
|
672
|
+
The script writes its report to that file (`{N}` = the cycle; do not save it yourself). Inject
|
|
673
|
+
the output into the reviewer's context as `--- VERIFICAÇÃO AUTOMÁTICA ---`. The reviewer must copy the
|
|
676
674
|
measured values from it (see best-practices `review.md`). If the checker did not run (no Node,
|
|
677
675
|
an error, or no `VERIFICACAO:` status line), tell the user, continue with the normal review and
|
|
678
676
|
repeat it at the final approval: "⚠️ A verificação automática não rodou: {motivo}".
|
|
@@ -695,11 +693,12 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
695
693
|
{any other status} A revisão não aprovou o texto depois de {N} ciclos. Motivo: {parecer resumido}
|
|
696
694
|
|
|
697
695
|
1. Corrigir eu mesmo (eu edito o texto e você verifica de novo)
|
|
698
|
-
2. Aceitar assim mesmo
|
|
696
|
+
2. Aceitar assim mesmo (fica registrado na entrega)
|
|
699
697
|
3. Abortar
|
|
700
698
|
```
|
|
701
699
|
5. **Final approval checkpoint** (the checkpoint after the review): show the summary of the last
|
|
702
|
-
report — `Verificação automática: {N} bloqueios, {M} alertas, {Z} não medidos
|
|
700
|
+
report — `Verificação automática: {N} bloqueios, {M} alertas, {Z} não medidos`, with
|
|
701
|
+
`{P} a preencher` right after the blocks when the report counts any — plus the list
|
|
703
702
|
of alerts and the {Z} items not measured or not verified (the `Não medido` and `Não verificado`
|
|
704
703
|
lines under each file, not the "não é texto" line of **Notas**), one per line as
|
|
705
704
|
`{arquivo} — {motivo}`, then the lines under `**Notas:**` in that report, as they are written,
|
|
@@ -707,7 +706,8 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
707
706
|
every file left unchecked by the safe-name rule. List the same way every file the path script
|
|
708
707
|
did not check (see Output Path Transformation). If the approved
|
|
709
708
|
text still contains `[PREENCHER: …]`, ask the user for each missing piece of real information
|
|
710
|
-
and write it into the text before approving.
|
|
709
|
+
and write it into the text before approving. If the user does not have it, do not insist and
|
|
710
|
+
never invent: keep the `[PREENCHER]`, say `Sem problema: deixo [PREENCHER: {o que falta}] no texto. Na entrega você escolhe entre preencher depois e entregar assim mesmo, com ressalva.` and go on.
|
|
711
711
|
|
|
712
712
|
### Step Execution Order (Summary)
|
|
713
713
|
|
|
@@ -728,7 +728,7 @@ Steps 1 and 4 are binary script gates. If either fails, the pipeline does NOT ad
|
|
|
728
728
|
|
|
729
729
|
### Entrega
|
|
730
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`,
|
|
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`) and copies what is ready to the folder of the project the user chose. 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`, `ENTREGA:COM_RESSALVA` and `ENTREGA:INCOMPLETA`, the question about the folder of the project that keeps a copy (asked once per crew) and what to do with a script that did not run.
|
|
732
732
|
|
|
733
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
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.
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Linha de comando do `entregar.mjs`: as opções, a lista `caminho=formato` e a linha de uso.
|
|
2
|
-
//
|
|
2
|
+
// Specs: fase-u3a1-pasta-de-entrega.md, §3 e §6, e fase-u3a2-entrega-no-projeto.md, §3
|
|
3
|
+
// (repositório do OpenCrew).
|
|
3
4
|
import { lerItemDaLista } from '../verificar/argumentos.mjs';
|
|
4
5
|
|
|
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
|
+
export const USO = 'Uso: node _opencrew/core/scripts/entregar.mjs --crew "crews/<crew>" --run "<id>" --arquivo "<caminho=formato>[,<caminho=formato>…]" [--destino "<pasta>"] [--lembrar-destino "<pasta>"|nao] [--aceitar-pendencias] [--vai-publicar <canal>] [--ajuda]';
|
|
6
7
|
|
|
7
|
-
const OPCAO = /^--(crew|run|arquivo|vai-publicar)(?:=(.*))?$/s;
|
|
8
|
+
const OPCAO = /^--(crew|run|arquivo|vai-publicar|destino|lembrar-destino)(?:=(.*))?$/s;
|
|
9
|
+
const CHAVE = { 'lembrar-destino': 'lembrarDestino' };
|
|
8
10
|
|
|
9
11
|
/** Texto que veio da linha de comando e volta numa mensagem: uma linha só, até 200 caracteres. */
|
|
10
12
|
export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
@@ -12,23 +14,25 @@ export const limpar = (valor) => String(valor).replace(/\s+/g, ' ').trim().slice
|
|
|
12
14
|
function guardar(args, nome, valor) {
|
|
13
15
|
if (nome === 'arquivo') args.arquivos.push(valor);
|
|
14
16
|
else if (nome === 'vai-publicar') args.vaiPublicar.push(valor.trim().toLowerCase());
|
|
15
|
-
else args[nome] = valor;
|
|
17
|
+
else args[CHAVE[nome] ?? nome] = valor;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
* `argv` → `{ crew, run, arquivos, vaiPublicar,
|
|
20
|
-
* `--nome valor` e `--nome=valor`. `--arquivo` e `--vai-publicar`
|
|
21
|
-
* solto logo depois da lista é mais um item dela. `
|
|
22
|
-
* não
|
|
21
|
+
* `argv` → `{ crew, run, arquivos, vaiPublicar, destino, lembrarDestino, aceitar, ajuda,
|
|
22
|
+
* desconhecida }`. Opção vale como `--nome valor` e `--nome=valor`. `--arquivo` e `--vai-publicar`
|
|
23
|
+
* repetidos somam, e o que vem solto logo depois da lista é mais um item dela. `destino` e
|
|
24
|
+
* `lembrarDestino` ficam sem valor (undefined) quando a opção não veio. `desconhecida`: a primeira
|
|
25
|
+
* opção que o script não conhece, ou null.
|
|
23
26
|
*/
|
|
24
27
|
export function lerArgs(argv) {
|
|
25
|
-
const args = { arquivos: [], vaiPublicar: [], ajuda: false, desconhecida: null };
|
|
28
|
+
const args = { arquivos: [], vaiPublicar: [], aceitar: false, ajuda: false, desconhecida: null };
|
|
26
29
|
let naLista = false;
|
|
27
30
|
for (let i = 0; i < argv.length; i++) {
|
|
28
31
|
const [, nome, colado] = argv[i].match(OPCAO) ?? [];
|
|
29
32
|
const solto = !nome && !argv[i].startsWith('--');
|
|
30
33
|
if (nome) guardar(args, nome, colado ?? (i + 1 < argv.length && !argv[i + 1].startsWith('--') ? argv[++i] : ''));
|
|
31
34
|
else if (argv[i] === '--ajuda') args.ajuda = true;
|
|
35
|
+
else if (argv[i] === '--aceitar-pendencias') args.aceitar = true;
|
|
32
36
|
else if (solto && naLista) args.arquivos.push(argv[i]);
|
|
33
37
|
else args.desconhecida ??= argv[i];
|
|
34
38
|
naLista = nome ? nome === 'arquivo' : solto && naLista;
|
|
@@ -35,17 +35,19 @@ export async function canalDoFormato(raiz, formato) {
|
|
|
35
35
|
const PASTAS_DE_SERVICO = new Set(['entrega', 'entrega.tmp', 'export']);
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Arquivo de serviço nunca entra, mesmo listado: `verificacao-*.md`, `
|
|
38
|
+
* Arquivo de serviço nunca entra, mesmo listado: `verificacao-*.md`, `ressalvas.json` (as
|
|
39
|
+
* pendências aceitas, fase-u3a2-entrega-no-projeto.md, regra 18), `publicado.json`, o que está
|
|
39
40
|
* em `entrega/`, `entrega.tmp/` ou `export/` de uma execução e o `caption.txt` direto na pasta
|
|
40
41
|
* da execução (é ali que o publicador o grava; dentro de uma pasta `vN` ele é saída de passo).
|
|
42
|
+
* E o `copia.json` direto na pasta da execução: o retrato do que foi copiado (mesma spec, regra 14).
|
|
41
43
|
* @param {string} rel caminho relativo ao projeto, com `/`
|
|
42
44
|
*/
|
|
43
45
|
export function ehDeServico(rel) {
|
|
44
46
|
const partes = rel.toLowerCase().split('/');
|
|
45
47
|
const nome = partes.at(-1);
|
|
46
|
-
if (/^verificacao-.*\.md$/.test(nome) || nome === 'publicado.json') return true;
|
|
48
|
+
if (/^verificacao-.*\.md$/.test(nome) || nome === 'publicado.json' || nome === 'ressalvas.json') return true;
|
|
47
49
|
const naSaida = partes[0] === 'crews' && partes[2] === 'output';
|
|
48
50
|
if (!naSaida) return false;
|
|
49
51
|
if (partes.slice(4, -1).some((p) => PASTAS_DE_SERVICO.has(p))) return true;
|
|
50
|
-
return nome === 'caption.txt' && partes.length === 5;
|
|
52
|
+
return (nome === 'caption.txt' || nome === 'copia.json') && partes.length === 5;
|
|
51
53
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Comparação entre o que a entrega copiaria agora e a cópia que já está no destino: é ela que
|
|
2
|
+
// decide se nada muda, se só há arquivos a acrescentar ou se a entrega vai para outra pasta.
|
|
3
|
+
// Com o retrato do que foi copiado (`copia.json`, ver `retrato.mjs`), a comparação é com ele: o que o
|
|
4
|
+
// usuário fez na cópia depois não conta. Sem retrato, é com os arquivos da pasta.
|
|
5
|
+
// Este módulo só lê. Spec: fase-u3a2-entrega-no-projeto.md, regra 14 (repositório do OpenCrew).
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// Em texto, CRLF e LF são o mesmo conteúdo (o editor ou a sincronização trocam); o resto, byte a byte.
|
|
11
|
+
const DE_TEXTO = new Set(['.txt', '.md', '.html', '.htm', '.csv', '.json']);
|
|
12
|
+
export const emLf = (texto) => texto.replace(/\r\n/g, '\n');
|
|
13
|
+
|
|
14
|
+
/** Os bytes que um arquivo da entrega tem: o texto gerado, ou os do arquivo de origem. */
|
|
15
|
+
export const bytesDe = async (a) => (a.texto != null ? Buffer.from(a.texto, 'utf8') : fs.readFile(a.de));
|
|
16
|
+
|
|
17
|
+
const ehTexto = (nome) => DE_TEXTO.has(path.extname(nome).toLowerCase());
|
|
18
|
+
const mesmoConteudo = (nome, a, b) => a.equals(b) || (ehTexto(nome) && emLf(a.toString('utf8')) === emLf(b.toString('utf8')));
|
|
19
|
+
|
|
20
|
+
/** Os arquivos sob uma pasta, com o prefixo dado e `/`; pasta que não existe: nenhum. */
|
|
21
|
+
async function listar(pasta, prefixo) {
|
|
22
|
+
const entradas = await fs.readdir(pasta, { withFileTypes: true }).catch(() => []);
|
|
23
|
+
const achados = [];
|
|
24
|
+
for (const e of entradas) {
|
|
25
|
+
if (e.isDirectory()) achados.push(...(await listar(path.join(pasta, e.name), `${prefixo}${e.name}/`)));
|
|
26
|
+
else achados.push(`${prefixo}${e.name}`);
|
|
27
|
+
}
|
|
28
|
+
return achados;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const REENTREGA = '-reentrega-';
|
|
32
|
+
|
|
33
|
+
/** 1 para `<run>`, N para `<run>-reentrega-N` (N ≥ 2), 0 para qualquer outro nome. */
|
|
34
|
+
function numeroDe(nome, run) {
|
|
35
|
+
if (nome === run) return 1;
|
|
36
|
+
const n = nome.startsWith(`${run}${REENTREGA}`) ? nome.slice(run.length + REENTREGA.length) : '';
|
|
37
|
+
return /^[1-9]\d*$/.test(n) && Number(n) >= 2 ? Number(n) : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* As pastas desta execução no destino, da mais antiga para a mais nova: `<run>` é a 1 e
|
|
42
|
+
* `<run>-reentrega-N` é a N. Só pastas: arquivo com um desses nomes não conta.
|
|
43
|
+
* @returns {Promise<{ n: number, nome: string }[]>}
|
|
44
|
+
*/
|
|
45
|
+
export async function pastasDaExecucao(destino, run) {
|
|
46
|
+
const entradas = await fs.readdir(destino, { withFileTypes: true }).catch(() => []);
|
|
47
|
+
const pastas = entradas.filter((e) => e.isDirectory()).map((e) => ({ n: numeroDe(e.name, run), nome: e.name }));
|
|
48
|
+
return pastas.filter((p) => p.n).sort((a, b) => a.n - b.n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hashDe = (nome, bytes) => createHash('sha1').update(ehTexto(nome) ? emLf(bytes.toString('utf8')) : bytes).digest('hex');
|
|
52
|
+
|
|
53
|
+
/** O retrato de uma lista de arquivos da entrega: `{ 'pasta/nome': hash }` (em texto, sem contar CRLF/LF). */
|
|
54
|
+
export async function retratoDe(arquivos) {
|
|
55
|
+
const retrato = {};
|
|
56
|
+
for (const a of arquivos) retrato[`${a.pasta}/${a.nome}`] = hashDe(a.nome, await bytesDe(a));
|
|
57
|
+
return retrato;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A mesma pergunta de `oQueFalta`, respondida pelo retrato do que foi copiado para a pasta. O que
|
|
62
|
+
* o usuário editou, apagou ou acrescentou na cópia não é diferença. Arquivo novo cujo lugar já
|
|
63
|
+
* está ocupado só conta como copiado quando o conteúdo é o mesmo; senão, é diferença.
|
|
64
|
+
*/
|
|
65
|
+
async function contraORetrato(pasta, esperados, canais, ignorar, retrato) {
|
|
66
|
+
const saiu = (rel) => canais.has(rel.split('/')[0]) && !ignorar.has(rel) && !esperados.has(rel);
|
|
67
|
+
if (Object.keys(retrato).some(saiu)) return null;
|
|
68
|
+
const faltam = [];
|
|
69
|
+
for (const [rel, a] of esperados) {
|
|
70
|
+
const bytes = await bytesDe(a);
|
|
71
|
+
if (Object.hasOwn(retrato, rel)) {
|
|
72
|
+
if (retrato[rel] !== hashDe(rel, bytes)) return null;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const noDisco = await fs.readFile(path.join(pasta, rel)).catch(() => null);
|
|
76
|
+
if (noDisco == null) faltam.push(a);
|
|
77
|
+
else if (!mesmoConteudo(rel, noDisco, bytes)) return null;
|
|
78
|
+
}
|
|
79
|
+
return faltam;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compara os arquivos que seriam copiados agora com uma pasta de cópia. Só as pastas de canal que
|
|
84
|
+
* seriam copiadas agora são olhadas: a raiz da cópia e as outras pastas ficam de fora.
|
|
85
|
+
* @param {string} pasta a cópia, absoluta · @param {object[]} arquivos `{ pasta, nome, texto | de }`
|
|
86
|
+
* @param {Set<string>} ignorar `pasta/nome` do que a entrega tem mas não copia agora
|
|
87
|
+
* @param {object|null} [retrato] o retrato do que foi copiado para esta pasta; null = comparar com os arquivos
|
|
88
|
+
* @returns {Promise<null | object[]>} null = diferente (arquivo que mudou, saiu ou está a mais);
|
|
89
|
+
* senão, os arquivos que faltam na cópia (nenhum = igual)
|
|
90
|
+
*/
|
|
91
|
+
export async function oQueFalta(pasta, arquivos, ignorar, retrato = null) {
|
|
92
|
+
const esperados = new Map(arquivos.map((a) => [`${a.pasta}/${a.nome}`, a]));
|
|
93
|
+
if (retrato) return contraORetrato(pasta, esperados, new Set(arquivos.map((a) => a.pasta)), ignorar, retrato);
|
|
94
|
+
const faltam = new Map(esperados);
|
|
95
|
+
for (const canal of new Set(arquivos.map((a) => a.pasta))) {
|
|
96
|
+
for (const rel of await listar(path.join(pasta, canal), `${canal}/`)) {
|
|
97
|
+
if (ignorar.has(rel)) continue;
|
|
98
|
+
const a = esperados.get(rel);
|
|
99
|
+
if (!a || !mesmoConteudo(rel, await fs.readFile(path.join(pasta, rel)), await bytesDe(a))) return null;
|
|
100
|
+
faltam.delete(rel);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...faltam.values()];
|
|
104
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// A cópia da entrega para a pasta do projeto que o usuário escolheu: uma pasta por execução
|
|
2
|
+
// (`<destino>/<run_id>/`). Nada é sobrescrito, menos o `LEIA-ME.md` da cópia, que é do script.
|
|
3
|
+
// Pasta nova é montada em `<pasta>.tmp/`, ao lado, e renomeada no fim: nada fica pela metade.
|
|
4
|
+
// O script só apaga o temporário que ele mesmo criou. O que o usuário edita na cópia fica: a
|
|
5
|
+
// entrega seguinte é comparada com o retrato do que foi copiado (`retrato.mjs`), não com a pasta.
|
|
6
|
+
// Spec: fase-u3a2-entrega-no-projeto.md, regras 14 e 15 (repositório do OpenCrew).
|
|
7
|
+
import { constants, existsSync, promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { emLf, oQueFalta, pastasDaExecucao } from './comparar.mjs';
|
|
10
|
+
|
|
11
|
+
export const AVISO = (pasta) => `Há uma entrega mais nova desta execução em \`${pasta}\`.`;
|
|
12
|
+
const AVISO_ANTERIOR = /^Há uma entrega mais nova desta execução em `[^`\n]*`\.\r?\n\r?\n/;
|
|
13
|
+
const LEIAME = 'LEIA-ME.md';
|
|
14
|
+
const apagar = (pasta) => fs.rm(pasta, { recursive: true, force: true, maxRetries: 3 });
|
|
15
|
+
const ehPasta = async (p) => fs.stat(p).then((s) => s.isDirectory(), () => false);
|
|
16
|
+
|
|
17
|
+
/** Grava um arquivo que ainda não existe ali: se existir, falha (nunca por cima). */
|
|
18
|
+
async function gravarNovo(alvo, a) {
|
|
19
|
+
await fs.mkdir(path.dirname(alvo), { recursive: true });
|
|
20
|
+
if (a.texto != null) await fs.writeFile(alvo, a.texto, { encoding: 'utf8', flag: 'wx' });
|
|
21
|
+
else await fs.copyFile(a.de, alvo, constants.COPYFILE_EXCL);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Monta a pasta nova no temporário e a põe no lugar; em falha, o temporário some. */
|
|
25
|
+
async function montar(pasta, arquivos, leiame, em) {
|
|
26
|
+
const tmp = `${pasta}.tmp`;
|
|
27
|
+
if (await ehPasta(tmp)) await apagar(tmp); // sobra de uma chamada interrompida
|
|
28
|
+
await fs.mkdir(tmp);
|
|
29
|
+
try {
|
|
30
|
+
for (const a of arquivos) {
|
|
31
|
+
em.alvo = path.join(pasta, a.pasta, a.nome);
|
|
32
|
+
await gravarNovo(path.join(tmp, a.pasta, a.nome), a);
|
|
33
|
+
}
|
|
34
|
+
em.alvo = path.join(pasta, LEIAME);
|
|
35
|
+
await fs.writeFile(path.join(tmp, LEIAME), leiame, 'utf8');
|
|
36
|
+
em.alvo = pasta;
|
|
37
|
+
await fs.rename(tmp, pasta);
|
|
38
|
+
} catch (erro) {
|
|
39
|
+
await apagar(tmp).catch(() => {});
|
|
40
|
+
throw erro;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A pasta mais alta do caminho que ainda não existe (é a que o script cria); null quando todas existem. */
|
|
45
|
+
function primeiraQueFalta(pasta) {
|
|
46
|
+
let falta = null;
|
|
47
|
+
for (let p = pasta; !existsSync(p); p = path.dirname(p)) falta = p;
|
|
48
|
+
return falta;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Desfaz as pastas vazias que esta chamada criou, de `pasta` até `criada`. */
|
|
52
|
+
async function desfazer(criada, pasta) {
|
|
53
|
+
for (let p = pasta; ; p = path.dirname(p)) {
|
|
54
|
+
await fs.rmdir(p);
|
|
55
|
+
if (p === criada) return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Cria `pasta` com a entrega inteira. @returns {Promise<boolean>} o destino precisou ser criado? */
|
|
60
|
+
async function criar(pasta, arquivos, leiame, em) {
|
|
61
|
+
em.alvo = pasta;
|
|
62
|
+
if (existsSync(pasta)) throw new Error('já existe um arquivo com o nome da pasta');
|
|
63
|
+
const destino = path.dirname(pasta);
|
|
64
|
+
const criada = primeiraQueFalta(destino);
|
|
65
|
+
try {
|
|
66
|
+
await fs.mkdir(destino, { recursive: true });
|
|
67
|
+
await montar(pasta, arquivos, leiame, em);
|
|
68
|
+
} catch (erro) {
|
|
69
|
+
if (criada) await desfazer(criada, destino).catch(() => {});
|
|
70
|
+
throw erro;
|
|
71
|
+
}
|
|
72
|
+
return Boolean(criada);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** O LEIA-ME da cópia é regravado quando muda (CRLF no lugar de LF não é mudança), sempre por último. */
|
|
76
|
+
async function regravarLeiame(pasta, leiame, em) {
|
|
77
|
+
em.alvo = path.join(pasta, LEIAME);
|
|
78
|
+
const atual = await fs.readFile(em.alvo, 'utf8').catch(() => null);
|
|
79
|
+
if (atual == null || emLf(atual) !== leiame) await fs.writeFile(em.alvo, leiame, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** As pastas anteriores ganham, na primeira linha do LEIA-ME, o aviso da pasta nova (um só). */
|
|
83
|
+
async function avisarAnteriores(destino, anteriores, pastaNova, em) {
|
|
84
|
+
for (const { nome } of anteriores) {
|
|
85
|
+
em.alvo = path.join(destino, nome, LEIAME);
|
|
86
|
+
const atual = await fs.readFile(em.alvo, 'utf8').catch(() => null);
|
|
87
|
+
if (atual != null) await fs.writeFile(em.alvo, `${AVISO(pastaNova)}\n\n${atual.replace(AVISO_ANTERIOR, '')}`, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** O LEIA-ME da pasta número `n` (1 = `<run>`; N = `<run>-reentrega-N`): um texto fixo, ou quem o monta. */
|
|
92
|
+
const leiameDe = (leiame, n) => (typeof leiame === 'function' ? leiame(n) : leiame);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Completa a pasta mais nova com o que falta, ou cria a pasta de reentrega quando algo mudou. A
|
|
96
|
+
* comparação é com o retrato do que foi copiado para ela; sem retrato, com os arquivos dela.
|
|
97
|
+
*/
|
|
98
|
+
async function atualizar({ destino, run, arquivos, ignorar, leiame, retratos }, existentes, em) {
|
|
99
|
+
const ultima = existentes.at(-1);
|
|
100
|
+
const pasta = path.join(destino.abs, ultima.nome);
|
|
101
|
+
const faltam = await oQueFalta(pasta, arquivos, ignorar, retratos?.[`${destino.rel}/${ultima.nome}`] ?? null);
|
|
102
|
+
if (!faltam) {
|
|
103
|
+
const nome = `${run}-reentrega-${ultima.n + 1}`;
|
|
104
|
+
await criar(path.join(destino.abs, nome), arquivos, leiameDe(leiame, ultima.n + 1), em);
|
|
105
|
+
await avisarAnteriores(destino.abs, existentes, `${destino.rel}/${nome}`, em);
|
|
106
|
+
return { tipo: 'reentrega', pasta: `${destino.rel}/${nome}` };
|
|
107
|
+
}
|
|
108
|
+
for (const a of faltam) {
|
|
109
|
+
em.alvo = path.join(pasta, a.pasta, a.nome);
|
|
110
|
+
await gravarNovo(em.alvo, a);
|
|
111
|
+
}
|
|
112
|
+
await regravarLeiame(pasta, leiameDe(leiame, ultima.n), em);
|
|
113
|
+
return { tipo: faltam.length ? 'completada' : 'igual', pasta: `${destino.rel}/${ultima.nome}`, novos: faltam.length };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Copia para o destino o que está pronto.
|
|
118
|
+
* @param {object} o
|
|
119
|
+
* @param {{ rel: string, abs: string }} o.destino a pasta escolhida, já validada · @param {string} o.run
|
|
120
|
+
* @param {object[]} o.arquivos o que é copiado agora: `{ pasta, nome, texto | de }`
|
|
121
|
+
* @param {Set<string>} o.ignorar `pasta/nome` do que a entrega tem e não é copiado agora
|
|
122
|
+
* @param {string|function(number): string} o.leiame o LEIA-ME da cópia, ou quem o monta para a pasta número N
|
|
123
|
+
* @param {object} [o.retratos] por pasta de cópia (relativa ao projeto), o retrato do que foi copiado
|
|
124
|
+
* @returns {Promise<object>} `{ tipo, pasta, novos, criouDestino }` — `tipo`: nova, igual,
|
|
125
|
+
* completada ou reentrega; `pasta`: relativa ao projeto · ou `{ tipo: 'falha', arquivo }`, com o
|
|
126
|
+
* caminho absoluto que não pôde ser gravado
|
|
127
|
+
*/
|
|
128
|
+
export async function copiar(o) {
|
|
129
|
+
const em = { alvo: o.destino.abs };
|
|
130
|
+
try {
|
|
131
|
+
const existentes = await pastasDaExecucao(o.destino.abs, o.run);
|
|
132
|
+
if (existentes.length) return await atualizar(o, existentes, em);
|
|
133
|
+
const criouDestino = await criar(path.join(o.destino.abs, o.run), o.arquivos, leiameDe(o.leiame, 1), em);
|
|
134
|
+
return { tipo: 'nova', pasta: `${o.destino.rel}/${o.run}`, criouDestino };
|
|
135
|
+
} catch {
|
|
136
|
+
return { tipo: 'falha', arquivo: em.alvo };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// O destino da cópia: a pasta do projeto que o usuário escolheu para guardar a entrega. Vem de
|
|
2
|
+
// `--lembrar-destino`, de `--destino` ou de `entrega.destino` no `crew.yaml`, nessa ordem, e uma
|
|
3
|
+
// função só valida os três. Este módulo só lê.
|
|
4
|
+
// Spec: fase-u3a2-entrega-no-projeto.md, §3 e regra 13 (repositório do OpenCrew).
|
|
5
|
+
import { existsSync, promises as fs, statSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { lugarReal } from '../comum.mjs';
|
|
8
|
+
import { limpar } from './argumentos.mjs';
|
|
9
|
+
|
|
10
|
+
export const MSG = {
|
|
11
|
+
recusado: (valor) => `Não copiei: o destino precisa ser uma pasta dentro do projeto, fora de \`_opencrew/\`, \`crews/\`, \`skills/\`, \`.git/\` e \`node_modules/\`. Recebi: ${limpar(valor)}.`,
|
|
12
|
+
semDestino: 'Cópia: nenhuma pasta escolhida para esta crew.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PROIBIDAS = new Set(['_opencrew', 'crews', 'skills', '.git', 'node_modules']);
|
|
16
|
+
const NAO = /^(?:nao|não|no)$/i;
|
|
17
|
+
// Absoluto: começa por `/`, `\` ou letra de unidade. Lista ou mapa do YAML não é caminho.
|
|
18
|
+
const NAO_E_CAMINHO = /^(?:[\\/]|[A-Za-z]:|[[{]|-(?:\s|$))/;
|
|
19
|
+
|
|
20
|
+
/** Relativo à raiz, com `/`; null quando sai do projeto, é a raiz ou cai numa pasta reservada. */
|
|
21
|
+
function dentro(raiz, alvo) {
|
|
22
|
+
const rel = path.relative(raiz, alvo);
|
|
23
|
+
if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
|
|
24
|
+
const partes = rel.split(path.sep);
|
|
25
|
+
return PROIBIDAS.has(partes[0].toLowerCase()) ? null : partes.join('/');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** O trecho do caminho que já existe é uma pasta? Um arquivo no caminho impede a cópia. */
|
|
29
|
+
function semArquivoNoCaminho(abs) {
|
|
30
|
+
let existente = abs;
|
|
31
|
+
while (!existsSync(existente)) existente = path.dirname(existente);
|
|
32
|
+
return statSync(existente).isDirectory();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Valida um destino (regra 13), venha de onde vier.
|
|
37
|
+
* @returns {{ tipo: 'nao' } | { tipo: 'recusado', valor: string } | { tipo: 'pasta', rel: string, abs: string }}
|
|
38
|
+
* `nao`: o usuário não quer cópia · `rel`: relativo ao projeto, com `/`
|
|
39
|
+
*/
|
|
40
|
+
export function validarDestino(raiz, valor) {
|
|
41
|
+
const texto = String(valor ?? '').trim();
|
|
42
|
+
if (NAO.test(texto)) return { tipo: 'nao' };
|
|
43
|
+
const recusado = { tipo: 'recusado', valor: texto };
|
|
44
|
+
if (!texto || NAO_E_CAMINHO.test(texto)) return recusado;
|
|
45
|
+
// \ vale como separador em qualquer sistema: o usuário digita Pasta\Sub também fora do Windows.
|
|
46
|
+
const abs = path.resolve(raiz, texto.replace(/\\/g, '/'));
|
|
47
|
+
const rel = dentro(raiz, abs);
|
|
48
|
+
// Pelo lugar real também: um atalho dentro do projeto pode levar para fora, ou para `crews/`.
|
|
49
|
+
if (!rel || !dentro(lugarReal(raiz), lugarReal(abs)) || !semArquivoNoCaminho(abs)) return recusado;
|
|
50
|
+
return { tipo: 'pasta', rel, abs };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const semFim = (linha) => linha.replace(/\r?\n$/, '');
|
|
54
|
+
const recuoDe = (linha) => linha.match(/^[ \t]*/)[0];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* O bloco `entrega:` de um `crew.yaml` já separado em linhas: onde começa e acaba, o recuo dos
|
|
58
|
+
* filhos e a linha de `destino:` (um nível abaixo; -1 quando não há). Sem o bloco: null.
|
|
59
|
+
*/
|
|
60
|
+
export function blocoDaEntrega(linhas) {
|
|
61
|
+
const limpas = linhas.map(semFim);
|
|
62
|
+
const inicio = limpas.findIndex((l) => /^entrega:\s*(?:#.*)?$/.test(l));
|
|
63
|
+
if (inicio < 0) return null;
|
|
64
|
+
let fim = inicio + 1;
|
|
65
|
+
while (fim < limpas.length && (!limpas[fim].trim() || /^[ \t]/.test(limpas[fim]))) fim += 1;
|
|
66
|
+
const filhos = limpas.slice(inicio + 1, fim).filter((l) => l.trim());
|
|
67
|
+
const recuo = filhos.length ? recuoDe(filhos[0]) : ' ';
|
|
68
|
+
const destino = limpas.findIndex((l, i) => i > inicio && i < fim && recuoDe(l) === recuo && /^destino:/.test(l.trim()));
|
|
69
|
+
return { inicio, fim, recuo, destino };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** O valor de uma linha `chave: valor`: sem as aspas e sem o comentário do fim da linha. */
|
|
73
|
+
function valorDaLinha(linha) {
|
|
74
|
+
const resto = linha.slice(linha.indexOf(':') + 1).trim();
|
|
75
|
+
const comAspas = resto.match(/^(["'])(.*?)\1\s*(?:#.*)?$/);
|
|
76
|
+
return comAspas ? comAspas[2] : resto.replace(/(?:^|\s+)#.*$/, '').trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `entrega.destino` como está escrito no texto de um `crew.yaml`; null quando não há. Uma lista volta como o primeiro item dela (`- a`), que nunca é um destino válido. */
|
|
80
|
+
export function destinoDoTexto(texto) {
|
|
81
|
+
const linhas = texto.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
82
|
+
const bloco = blocoDaEntrega(linhas);
|
|
83
|
+
if (!bloco || bloco.destino < 0) return null;
|
|
84
|
+
const valor = valorDaLinha(linhas[bloco.destino]);
|
|
85
|
+
if (valor) return valor;
|
|
86
|
+
const seguinte = (linhas[bloco.destino + 1] ?? '').trim();
|
|
87
|
+
return seguinte.startsWith('-') ? seguinte : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* O destino desta chamada: `--lembrar-destino` vence `--destino`, que vence o `crew.yaml`.
|
|
92
|
+
* @returns {Promise<object>} o que `validarDestino` devolve, ou `{ tipo: 'nenhum' }` (ninguém escolheu)
|
|
93
|
+
*/
|
|
94
|
+
export async function escolherDestino(raiz, crew, args) {
|
|
95
|
+
const doYaml = async () => destinoDoTexto(await fs.readFile(path.join(crew, 'crew.yaml'), 'utf8'));
|
|
96
|
+
const valor = args.lembrarDestino ?? args.destino ?? (await doYaml());
|
|
97
|
+
return valor == null ? { tipo: 'nenhum' } : validarDestino(raiz, valor);
|
|
98
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Blocos de rótulo do arquivo de origem que não chegam à entrega: os de serviço (notas, checklist,
|
|
2
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
3
|
// de um carrossel, por exemplo). A entrega não muda: o LEIA-ME só passa a dizer o que ficou fora.
|
|
4
|
-
//
|
|
4
|
+
// Specs: fase-u3a1-pasta-de-entrega.md, §4 e §6, ajuste da execução real, e
|
|
5
|
+
// fase-u3a2-entrega-no-projeto.md, §6: o aviso do LEIA-ME cita o arquivo de origem (repositório do OpenCrew).
|
|
5
6
|
import { semFrontmatter } from '../verificar/leitura.mjs';
|
|
6
7
|
import { ROTULOS } from '../verificar/pecas.mjs';
|
|
7
8
|
import { lerTrechos, marcar } from '../verificar/secoes.mjs';
|
|
@@ -9,7 +10,7 @@ import { PRINCIPAL } from './leitor.mjs';
|
|
|
9
10
|
import { ehServico, secoesDeRotulo, semComentarios } from './texto.mjs';
|
|
10
11
|
|
|
11
12
|
export const MSG = {
|
|
12
|
-
fora: (blocos) => `Ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem
|
|
13
|
+
fora: (blocos, arquivo) => `Ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem, \`${arquivo}\`.`,
|
|
13
14
|
foraNaTela: (arquivo, blocos) => `${arquivo}: ficou fora do texto para colar: ${blocos.join(', ')}. Veja no arquivo de origem.`,
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -45,5 +46,5 @@ export function blocosFora(texto, formato = null) {
|
|
|
45
46
|
|
|
46
47
|
/** O aviso de um arquivo: `{ pasta, texto, tela }`, ou nenhum quando nada ficou fora. */
|
|
47
48
|
export function avisoDeFora(item, blocos) {
|
|
48
|
-
return blocos.length ? [{ pasta: item.canal, texto: MSG.fora(blocos), tela: MSG.foraNaTela(item.rel, blocos) }] : [];
|
|
49
|
+
return blocos.length ? [{ pasta: item.canal, texto: MSG.fora(blocos, item.rel), tela: MSG.foraNaTela(item.rel, blocos) }] : [];
|
|
49
50
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Guardar a entrega no projeto: decide o que está pronto para ser copiado, chama a cópia e diz,
|
|
2
|
+
// em uma ou duas linhas do resumo, o que aconteceu. Canal com pendência não aceita fica de fora,
|
|
3
|
+
// e o HTML editável dele também. Depois da cópia, guarda o retrato do que foi copiado (`copia.json`).
|
|
4
|
+
// Spec: fase-u3a2-entrega-no-projeto.md, regras 13 a 15 e §6 (repositório do OpenCrew).
|
|
5
|
+
import { relativoAoProjeto } from '../comum.mjs';
|
|
6
|
+
import { EDITAVEIS } from './canais.mjs';
|
|
7
|
+
import { retratoDe } from './comparar.mjs';
|
|
8
|
+
import { copiar } from './copia.mjs';
|
|
9
|
+
import { MSG as DESTINO } from './destino.mjs';
|
|
10
|
+
import { montarLeiame } from './leiame.mjs';
|
|
11
|
+
import { gravarRetratos, lerRetratos } from './retrato.mjs';
|
|
12
|
+
|
|
13
|
+
export const MSG = {
|
|
14
|
+
falhaDeEscrita: (arquivo) => `Não consegui gravar ${arquivo}. Feche o arquivo, ou espere a sincronização da pasta, e rode de novo.`,
|
|
15
|
+
criei: (destino) => `Criei a pasta ${destino}.`,
|
|
16
|
+
nova: (pasta) => `Cópia: ${pasta}`,
|
|
17
|
+
igual: (pasta) => `Cópia: ${pasta} — já está atualizada.`,
|
|
18
|
+
completada: (pasta, n) => `Cópia: ${pasta} — completei com ${n} ${n === 1 ? 'arquivo novo' : 'arquivos novos'}.`,
|
|
19
|
+
reentrega: (pasta) => `Cópia: ${pasta} — guardei aqui porque a entrega mudou. Os arquivos da anterior ficaram como estavam; só o LEIA-ME dela ganhou um aviso.`,
|
|
20
|
+
nada: 'Cópia: nada foi copiado, porque nenhum canal está pronto.',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** A pasta cuja situação decide se o arquivo é copiado: a do canal (o editável segue o canal dele). */
|
|
24
|
+
const canalDe = (a) => (a.pasta === EDITAVEIS ? a.doCanal : a.pasta);
|
|
25
|
+
|
|
26
|
+
/** Pasta criada nesta chamada: o retrato dela começa do zero. */
|
|
27
|
+
const ehNova = (r) => r.tipo === 'nova' || r.tipo === 'reentrega';
|
|
28
|
+
|
|
29
|
+
/** As linhas do resumo para o que a cópia devolveu. */
|
|
30
|
+
function linhasDe(r, destino) {
|
|
31
|
+
if (r.tipo === 'nova') return [...(r.criouDestino ? [MSG.criei(destino.rel)] : []), MSG.nova(r.pasta)];
|
|
32
|
+
return [MSG[r.tipo](r.pasta, r.novos)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copia para o destino o que está pronto e descreve o resultado.
|
|
37
|
+
* @param {string} raiz pasta do projeto · @param {object} destino o que `escolherDestino` devolveu
|
|
38
|
+
* @param {object} dados os dados da entrega (os do LEIA-ME)
|
|
39
|
+
* @param {string} execucao a pasta da execução, absoluta: é nela que fica o retrato da cópia
|
|
40
|
+
* @returns {Promise<{ linhas: string[], pasta: string|null, falhou: boolean }>} `linhas`: as do
|
|
41
|
+
* resumo · `pasta`: a pasta da cópia, relativa ao projeto, quando há uma · `falhou`: destino
|
|
42
|
+
* recusado ou gravação que falhou (o final é ENTREGA:INCOMPLETA)
|
|
43
|
+
*/
|
|
44
|
+
export async function guardar(raiz, destino, dados, execucao) {
|
|
45
|
+
if (destino.tipo === 'nao') return { linhas: [], pasta: null, falhou: false };
|
|
46
|
+
if (destino.tipo === 'nenhum') return { linhas: [DESTINO.semDestino], pasta: null, falhou: false };
|
|
47
|
+
if (destino.tipo === 'recusado') return { linhas: [DESTINO.recusado(destino.valor)], pasta: null, falhou: true };
|
|
48
|
+
const pronto = (a) => !dados.pendencias.has(canalDe(a));
|
|
49
|
+
const arquivos = dados.arquivos.filter(pronto);
|
|
50
|
+
if (!arquivos.length) return { linhas: [MSG.nada], pasta: null, falhou: false };
|
|
51
|
+
const ignorar = new Set(dados.arquivos.filter((a) => !pronto(a)).map((a) => `${a.pasta}/${a.nome}`));
|
|
52
|
+
const leiame = (n) => montarLeiame({ ...dados, arquivos, ehCopia: true, reentrega: n });
|
|
53
|
+
const retratos = await lerRetratos(execucao);
|
|
54
|
+
const r = await copiar({ destino, run: dados.run, arquivos, ignorar, leiame, retratos });
|
|
55
|
+
if (r.tipo === 'falha') return { linhas: [MSG.falhaDeEscrita(relativoAoProjeto(raiz, r.arquivo))], pasta: null, falhou: true };
|
|
56
|
+
// O retrato da pasta: o que já estava nele e o que esta entrega tem (copiado agora ou igual ao que foi).
|
|
57
|
+
const semRetrato = await gravarRetratos(execucao, { ...retratos, [r.pasta]: { ...(ehNova(r) ? {} : retratos[r.pasta]), ...(await retratoDe(arquivos)) } });
|
|
58
|
+
const falha = semRetrato ? [MSG.falhaDeEscrita(relativoAoProjeto(raiz, semRetrato))] : [];
|
|
59
|
+
return { linhas: [...linhasDe(r, destino), ...falha], pasta: r.pasta, falhou: falha.length > 0 };
|
|
60
|
+
}
|