@orkastery/cli 0.5.1 → 0.5.3

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.
Files changed (110) hide show
  1. package/adapters/claude-code/agents/ork-master.md +2 -0
  2. package/adapters/claude-code/commands/master.md +8 -0
  3. package/adapters/claude-code/commands/ork.md +9 -0
  4. package/adapters/codex/skills/ork/SKILL.md +18 -3
  5. package/adapters/hermes/README.md +1 -0
  6. package/adapters/hermes/bin/ork-network-status.sh +10 -0
  7. package/adapters/hermes/hermes.plugin.json +1 -0
  8. package/adapters/hermes/hitl-ingress/__init__.py +2 -2
  9. package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +6 -5
  10. package/adapters/openclaw/README.md +24 -4
  11. package/adapters/openclaw/dist/hitl-ingress.js +2 -2
  12. package/adapters/openclaw/dist/index.js +46 -11
  13. package/adapters/openclaw/openclaw.plugin.json +5 -1
  14. package/adapters/openclaw/package.json +2 -2
  15. package/adapters/openclaw/src/hitl-ingress.ts +2 -2
  16. package/adapters/openclaw/src/index.ts +49 -11
  17. package/assets/orkmind_bridge.py +117 -9
  18. package/dist/adapters/claude-bg.js +39 -5
  19. package/dist/adapters/codex.js +4 -3
  20. package/dist/agents-md.js +1 -0
  21. package/dist/auditrun.js +5 -1
  22. package/dist/board.js +16 -14
  23. package/dist/branch-de-estado.js +66 -10
  24. package/dist/busca-semantica.js +52 -12
  25. package/dist/canarios-rm057.js +149 -0
  26. package/dist/canarios.js +2 -0
  27. package/dist/ci.js +8 -1
  28. package/dist/claims.js +39 -1
  29. package/dist/company-brain-context.js +158 -22
  30. package/dist/docs.js +48 -9
  31. package/dist/doctor-pulse-cron.js +194 -0
  32. package/dist/doctor-rede.js +149 -0
  33. package/dist/doctor.js +89 -9
  34. package/dist/entrega-pr.js +20 -8
  35. package/dist/esgotamentos.js +181 -0
  36. package/dist/experiencia.js +12 -2
  37. package/dist/fabrica-estado.js +14 -6
  38. package/dist/fabrica-publicar.js +6 -0
  39. package/dist/fechamento.js +20 -66
  40. package/dist/forja.js +31 -7
  41. package/dist/gates.js +5 -1
  42. package/dist/hitl-contract.js +59 -4
  43. package/dist/hitl-curto.js +11 -6
  44. package/dist/hitl-gates.js +5 -0
  45. package/dist/hitl-lote.js +5 -2
  46. package/dist/hitl-resumo.js +9 -0
  47. package/dist/hitl-tempo-parado.js +201 -0
  48. package/dist/hitl-texto-livre.js +5 -5
  49. package/dist/horario.js +27 -0
  50. package/dist/hosts.js +52 -10
  51. package/dist/impedimento.js +205 -0
  52. package/dist/index.js +391 -74
  53. package/dist/indice-vetorial.js +87 -10
  54. package/dist/indice.js +3 -1
  55. package/dist/init.js +17 -0
  56. package/dist/integracoes-locais.js +2 -0
  57. package/dist/intelligence-graph-cli.js +91 -8
  58. package/dist/intelligence-graph-contexto.js +215 -0
  59. package/dist/intelligence-graph-parsers.js +6 -4
  60. package/dist/leases.js +664 -87
  61. package/dist/ledger-stats.js +15 -1
  62. package/dist/ledger.js +3 -0
  63. package/dist/licoes.js +1 -0
  64. package/dist/maestro-runtime.js +4 -1
  65. package/dist/manifest.js +20 -1
  66. package/dist/master-nota.js +14 -5
  67. package/dist/master.js +129 -12
  68. package/dist/mcp-artifacts.js +34 -3
  69. package/dist/mcp-git.js +20 -3
  70. package/dist/mcp-grafo-worker.js +5 -3
  71. package/dist/mcp-grafo.js +50 -4
  72. package/dist/mcp-server.js +14 -1
  73. package/dist/memoria.js +48 -6
  74. package/dist/network-roadmap.js +191 -16
  75. package/dist/ocupacao.js +11 -3
  76. package/dist/orkmind.js +90 -5
  77. package/dist/orquestracao.js +12 -10
  78. package/dist/parado-no-condutor.js +188 -32
  79. package/dist/phase.js +68 -15
  80. package/dist/policies.js +52 -1
  81. package/dist/portfolio.js +4 -1
  82. package/dist/projeto-alvo.js +5 -1
  83. package/dist/prompts.js +36 -1
  84. package/dist/prova-ativacao.js +75 -6
  85. package/dist/pulse-delivery.js +34 -2
  86. package/dist/pulse.js +49 -3
  87. package/dist/ratelimit.js +20 -4
  88. package/dist/recall.js +3 -1
  89. package/dist/rede-adesao.js +399 -0
  90. package/dist/rede-forja.js +289 -0
  91. package/dist/rede-projetos.js +239 -0
  92. package/dist/rede-status.js +336 -0
  93. package/dist/rede.js +980 -0
  94. package/dist/retry.js +69 -2
  95. package/dist/roadmap-status.js +23 -3
  96. package/dist/runtime-profiles.js +22 -1
  97. package/dist/saida-segura.js +58 -0
  98. package/dist/session-watcher-claude.js +113 -23
  99. package/dist/sessoes-contas.js +187 -0
  100. package/dist/sessoes-inventario.js +6 -18
  101. package/dist/sessoes.js +83 -2
  102. package/dist/ship.js +85 -19
  103. package/dist/thread.js +192 -13
  104. package/dist/worktree.js +5 -2
  105. package/eval/fixtures/fx-pedido-colado/caso.json +20 -0
  106. package/package.json +7 -3
  107. package/references/definition-of-done.md +1 -1
  108. package/skills/README.md +1 -1
  109. package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
  110. package/skills/phases/master-metrics/SKILL.md +8 -3
package/dist/rede.js ADDED
@@ -0,0 +1,980 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RETRATO_PARADO_MS = exports.cachePronto = exports.MAXIMO_DE_ITENS = exports.refDaCasa = exports.PULSACAO_DA_REDE_MS = exports.PAINEL_DA_REDE = exports.DIR_DOS_RETRATOS = exports.BRANCH_DA_REDE = exports.CONTRATO_DO_RETRATO = void 0;
37
+ exports.nomeSeguro = nomeSeguro;
38
+ exports.arquivoDoRetrato = arquivoDoRetrato;
39
+ exports.resolverCasa = resolverCasa;
40
+ exports.hostnameSeguro = hostnameSeguro;
41
+ exports.retratoDaMaquina = retratoDaMaquina;
42
+ exports.retratoComDescartes = retratoComDescartes;
43
+ exports.assinaturaDoRetrato = assinaturaDoRetrato;
44
+ exports.achadoDeSegredo = achadoDeSegredo;
45
+ exports.exigirRetratoSeguro = exigirRetratoSeguro;
46
+ exports.segredoNoRetrato = segredoNoRetrato;
47
+ exports.exigirSoOProprioRetrato = exigirSoOProprioRetrato;
48
+ exports.normalizarRetrato = normalizarRetrato;
49
+ exports.projetoNoContrato = projetoNoContrato;
50
+ exports.retratosDaPonta = retratosDaPonta;
51
+ exports.dirDoCache = dirDoCache;
52
+ exports.prepararCache = prepararCache;
53
+ exports.urlDaCasaAceita = urlDaCasaAceita;
54
+ exports.identidadeDoGit = identidadeDoGit;
55
+ exports.lerMarcaDaRede = lerMarcaDaRede;
56
+ exports.gravarMarca = gravarMarca;
57
+ exports.tirarTravaOrfa = tirarTravaOrfa;
58
+ exports.idNoArquivo = idNoArquivo;
59
+ exports.publicarRede = publicarRede;
60
+ exports.publicarRedeNaBatida = publicarRedeNaBatida;
61
+ exports.entrarNaRede = entrarNaRede;
62
+ exports.sairDaRede = sairDaRede;
63
+ exports.retratoParado = retratoParado;
64
+ exports.painelDaRede = painelDaRede;
65
+ /**
66
+ * RM-053: a Orkastery Network, as maquinas de uma pessoa em rede (lado da escrita).
67
+ *
68
+ * D1 (ADR-001): a rede mora num repositorio PRIVADO da pessoa na forja, `<usuario>/orkastery-network`,
69
+ * branch `main`, um `maquinas/<maquina>.json` por maquina (`ork.rede-maquina/v1`) e o `REDE.md`.
70
+ * A gravacao e a da RM-047 (`branch-de-estado`): indice temporario, commit sobre a ponta lida, push
71
+ * sem forca e releitura quando recusado, num cache em `~/.orkastery/rede/` (arvore de trabalho vazia),
72
+ * fora de qualquer clone de projeto.
73
+ *
74
+ * D3: publicar exige o repositorio privado, conferido na forja antes de cada publicacao.
75
+ * D5: o retrato e uma LISTA DE PERMISSAO (maquina, hostname, forjas, runtimes, hosts, projetos,
76
+ * versao do `ork`, batida) e passa por uma varredura de segredo antes do push: um achado recusa a
77
+ * publicacao inteira, e o erro diz o padrao e o campo, nunca o valor.
78
+ * D6: cada maquina so escreve o proprio retrato; o `REDE.md` e indice derivado que quem publica
79
+ * regenera. A forja nao distingue maquinas da mesma pessoa, entao a garantia e daqui e a auditoria
80
+ * e do git.
81
+ * D9: retrato igual so volta ao remoto de hora em hora (a batida); a leitura fica em `rede-status`.
82
+ */
83
+ const node_crypto_1 = require("node:crypto");
84
+ const fs = __importStar(require("node:fs"));
85
+ const os = __importStar(require("node:os"));
86
+ const path = __importStar(require("node:path"));
87
+ const branch_de_estado_1 = require("./branch-de-estado");
88
+ const horario_1 = require("./horario");
89
+ const hosts_1 = require("./hosts");
90
+ const maquina_1 = require("./maquina");
91
+ const monitor_lock_1 = require("./monitor-lock");
92
+ const policies_1 = require("./policies");
93
+ const rede_adesao_1 = require("./rede-adesao");
94
+ const rede_forja_1 = require("./rede-forja");
95
+ const rede_projetos_1 = require("./rede-projetos");
96
+ const versao_1 = require("./versao");
97
+ exports.CONTRATO_DO_RETRATO = 'ork.rede-maquina/v1';
98
+ exports.BRANCH_DA_REDE = 'main';
99
+ exports.DIR_DOS_RETRATOS = 'maquinas';
100
+ exports.PAINEL_DA_REDE = 'REDE.md';
101
+ /** D9: retrato igual so volta ao remoto depois disto; e o sinal de vida da maquina. */
102
+ exports.PULSACAO_DA_REDE_MS = 60 * 60 * 1000;
103
+ const PREFIXO = 'rede';
104
+ const TENTATIVAS = 5;
105
+ const DESCRICAO = 'Orkastery Network: o retrato de cada maquina desta pessoa (RM-053). Gerado pelo ork; nao edite a mao.';
106
+ /** Os runtimes que o `ork` despacha, pelo binario que cada um usa. */
107
+ const RUNTIMES_DA_REDE = [['claude-bg', 'claude'], ['codex', 'codex']];
108
+ /** O binario de cada host de conducao. */
109
+ const BINARIO_DO_HOST = { 'claude-code': 'claude', codex: 'codex', hermes: 'hermes', openclaw: 'openclaw' };
110
+ const VERSAO_ESTRITA = /^\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.]{1,20})?$/;
111
+ const refDaCasa = (c) => `${c.host}/${c.dono}/${c.repositorio}`;
112
+ exports.refDaCasa = refDaCasa;
113
+ /**
114
+ * O nome da maquina no que vai a forja: so o que o git e a forja aceitam sem surpresa. E o mesmo no
115
+ * arquivo, no campo `maquina` do retrato, na trava e nos commits (S2 da revisao 2: o leitor recusava o
116
+ * retrato de nome longo ou com espaco que o escritor publicava).
117
+ */
118
+ function nomeSeguro(maquina) {
119
+ const nome = maquina.trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '').slice(0, 64);
120
+ // U3 da revisao 3: nome so com caracteres fora do ASCII (`日本`) ou so com pontos nao lanca mais; vira
121
+ // um nome estavel derivado dele, para o status e o sair continuarem funcionando.
122
+ return nome || `maquina-${(0, node_crypto_1.createHash)('sha256').update(maquina.trim()).digest('hex').slice(0, 8)}`;
123
+ }
124
+ /** O arquivo do retrato da maquina na casa. */
125
+ function arquivoDoRetrato(maquina) {
126
+ return `${exports.DIR_DOS_RETRATOS}/${nomeSeguro(maquina)}.json`;
127
+ }
128
+ /**
129
+ * A casa: a opcao do comando, senao a adesao gravada, senao o login da primeira forja com login.
130
+ * Nunca lanca: sem forja ou sem login, devolve o motivo tipado.
131
+ */
132
+ function resolverCasa(opcoes = {}) {
133
+ const amb = opcoes.amb ?? {};
134
+ const forjas = (0, rede_forja_1.forjasDaMaquina)(amb);
135
+ const identidades = opcoes.identidades ?? forjas.map((f) => f.identidade());
136
+ const config = (0, rede_adesao_1.lerConfigDaRede)();
137
+ const [donoPedido, nomePedido] = opcoes.repositorio?.includes('/') ? opcoes.repositorio.split('/', 2) : [null, opcoes.repositorio ?? null];
138
+ // B3: `--repositorio` sozinho troca so o repositorio; a forja gravada continua valendo.
139
+ const nomeDaForja = opcoes.forja ?? config?.forja ?? null;
140
+ if (!opcoes.forja && !opcoes.repositorio && config?.forja && config.dono && config.repositorio && config.host) {
141
+ // B9: a forja da casa e a do host gravado (GitLab proprio), nao a do ambiente de quem chamou.
142
+ const forja = (0, rede_forja_1.forjaPorNome)(config.forja, amb, config.host);
143
+ return { casa: { forja: config.forja, host: config.host, dono: config.dono, repositorio: config.repositorio, origem: 'rede.json' },
144
+ forja, identidades, motivo: forja ? null : { tipo: 'forja.ausente', detalhe: `a CLI da forja ${config.forja} nao esta nesta maquina` } };
145
+ }
146
+ // A forja gravada vale com o host gravado (GitLab proprio), mesmo quando so o repositorio muda.
147
+ const daCasaGravada = nomeDaForja && config?.forja === nomeDaForja && config.host ? (0, rede_forja_1.forjaPorNome)(nomeDaForja, amb, config.host) : null;
148
+ const candidatas = daCasaGravada ? [daCasaGravada] : nomeDaForja ? forjas.filter((f) => f.nome === nomeDaForja) : forjas;
149
+ if (candidatas.length === 0) {
150
+ return { casa: null, forja: null, identidades, motivo: { tipo: 'forja.ausente',
151
+ detalhe: nomeDaForja ? `a CLI da forja ${nomeDaForja} nao esta nesta maquina` : 'nenhuma CLI de forja (gh ou glab) nesta maquina' } };
152
+ }
153
+ const falhas = [];
154
+ for (const forja of candidatas) {
155
+ const lida = identidades.find((i) => i.forja === forja.nome && i.host === forja.host) ?? forja.identidade();
156
+ const usuario = lida.usuario;
157
+ if (!usuario) {
158
+ if (lida.falha)
159
+ falhas.push(lida.falha);
160
+ continue;
161
+ }
162
+ const origem = opcoes.forja || opcoes.repositorio ? 'opcao' : 'forja';
163
+ return { casa: { forja: forja.nome, host: forja.host, dono: donoPedido ?? usuario, repositorio: nomePedido ?? rede_adesao_1.REPOSITORIO_PADRAO, origem },
164
+ forja, identidades, motivo: null };
165
+ }
166
+ // Suspeitas da revisao de 03/10: prazo, erro da forja ou falta de rede no `gh api user` nao e falta de
167
+ // login; mandar a pessoa ao `gh auth login` esconderia o motivo de verdade.
168
+ if (falhas.length) {
169
+ return { casa: null, forja: null, identidades, motivo: { tipo: 'rede.sem-leitura',
170
+ detalhe: `${falhas.join('; ')}; o login nao foi lido, rode de novo com a forja respondendo` } };
171
+ }
172
+ return { casa: null, forja: null, identidades, motivo: { tipo: 'forja.sem-login',
173
+ detalhe: `${candidatas.map((f) => f.cli).join(' e ')} sem login nesta maquina (${candidatas.map((f) => `${f.cli} auth login`).join(' ou ')})` } };
174
+ }
175
+ function versaoDoAdaptador(host, home) {
176
+ const recibo = (0, hosts_1.lerRecibo)(path.join(home, hosts_1.HOSTS[host].destinoPadrao, hosts_1.HOSTS[host].subdir));
177
+ return typeof recibo?.versao === 'string' && VERSAO_ESTRITA.test(recibo.versao) ? recibo.versao : null;
178
+ }
179
+ function hostnameSeguro() {
180
+ const bruto = os.hostname().trim();
181
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/.test(bruto) ? bruto : bruto.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 253) || 'desconhecido';
182
+ }
183
+ /** O retrato desta maquina, montado so com os campos da lista de permissao (D5). */
184
+ function retratoDaMaquina(opcoes = {}) {
185
+ return retratoComDescartes(opcoes).retrato;
186
+ }
187
+ /**
188
+ * O retrato e os projetos que ficaram fora dele. Projeto vem de fora do nucleo (o registro da
189
+ * RM-052, o cwd, o ultimo retrato): um valor dele com cara de segredo tira SO aquele projeto, com
190
+ * aviso, em vez de travar a publicacao da maquina para sempre (A1 do CHECK 1). O que o nucleo monta
191
+ * (maquina, hostname, forjas, runtimes, hosts) continua falhando fechado em `exigirRetratoSeguro`.
192
+ */
193
+ function retratoComDescartes(opcoes = {}) {
194
+ const amb = opcoes.amb ?? {};
195
+ const home = amb.home ?? os.homedir();
196
+ const identidades = opcoes.identidades ?? (0, rede_forja_1.forjasDaMaquina)(amb).map((f) => f.identidade());
197
+ const versoes = new Map();
198
+ const binario = (nome) => {
199
+ if (!versoes.has(nome)) {
200
+ const achado = (0, rede_forja_1.acharBinario)(nome, amb);
201
+ versoes.set(nome, achado ? { binario: nome, versao: (0, rede_forja_1.versaoDoBinario)(achado, amb) } : null);
202
+ }
203
+ return versoes.get(nome) ?? null;
204
+ };
205
+ const runtimes = RUNTIMES_DA_REDE.flatMap(([runtime, nome]) => {
206
+ const b = binario(nome);
207
+ return b ? [{ runtime, binario: nome, versao: b.versao }] : [];
208
+ });
209
+ const hosts = hosts_1.ORDEM_DOS_HOSTS.flatMap((host) => {
210
+ const b = binario(BINARIO_DO_HOST[host]);
211
+ return b ? [{ host, versao: b.versao, adaptador: versaoDoAdaptador(host, home) }] : [];
212
+ });
213
+ const { projetos } = (0, rede_projetos_1.projetosConhecidos)({ arquivo: opcoes.arquivoDeProjetos, diretorio: opcoes.diretorio, anteriores: opcoes.anteriores });
214
+ const descartados = [];
215
+ // S2 da revisao 2: so vai o que o leitor aceita. Remoto longo demais vira `null` (e informativo);
216
+ // nome ou caminho fora do contrato tiram o projeto, com aviso; no maximo 200 projetos.
217
+ const presentes = projetos.filter((p) => p.presente)
218
+ .map((p) => ({ nome: p.nome, remoto: p.remoto !== null && p.remoto.length <= 500 ? p.remoto : null, caminho: p.caminho }));
219
+ const limpos = presentes.flatMap((p, i) => {
220
+ // V3 da revisao 4 e W1 da revisao 5: a regra unica de nome e o MESMO item de projeto do leitor
221
+ // (`projetoNoContrato`); vai ao retrato o que o leitor devolve, e o que ele recusa sai sozinho.
222
+ const lido = (0, rede_projetos_1.ehNomeDeProjeto)(p.nome) ? projetoNoContrato(p) : null;
223
+ if (!lido) {
224
+ descartados.push({ campo: `projetos[${i}]`, padrao: 'fora do contrato ork.rede-maquina/v1' });
225
+ return [];
226
+ }
227
+ for (const campo of ['nome', 'remoto', 'caminho']) {
228
+ const padrao = typeof lido[campo] === 'string' ? achadoDeSegredo(lido[campo]) : null;
229
+ if (padrao) {
230
+ descartados.push({ campo: `projetos[${i}].${campo}`, padrao });
231
+ return [];
232
+ }
233
+ }
234
+ return [lido];
235
+ });
236
+ if (limpos.length > exports.MAXIMO_DE_ITENS) {
237
+ descartados.push({ campo: `projetos[${exports.MAXIMO_DE_ITENS}..]`, padrao: `limite de ${exports.MAXIMO_DE_ITENS} projetos` });
238
+ limpos.length = exports.MAXIMO_DE_ITENS;
239
+ }
240
+ const retrato = {
241
+ contrato: exports.CONTRATO_DO_RETRATO,
242
+ maquina: nomeSeguro((0, maquina_1.nomeDaMaquina)(opcoes.maquina)),
243
+ ...(opcoes.id ? { id: opcoes.id } : {}),
244
+ hostname: hostnameSeguro(),
245
+ adesao: opcoes.adesao ?? (0, rede_adesao_1.adesaoDaRede)().adesao ?? 'rede',
246
+ forjas: identidades.map((i) => ({ forja: i.forja, host: i.host, cli: i.cli, versao: i.versao, usuario: i.usuario })),
247
+ runtimes,
248
+ hosts,
249
+ projetos: limpos,
250
+ versaoOrk: versao_1.VERSAO_DO_ORK,
251
+ publicadoEm: opcoes.agora ?? new Date().toISOString(),
252
+ };
253
+ return { retrato, descartados };
254
+ }
255
+ /** O que muda o retrato (sem o carimbo de hora): retrato igual nao precisa de push. */
256
+ function assinaturaDoRetrato(r) {
257
+ const { publicadoEm: _publicadoEm, ...resto } = r;
258
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(resto)).digest('hex');
259
+ }
260
+ // ---------------------------------------------------------------------------
261
+ // As guardas (D5 e D6).
262
+ // ---------------------------------------------------------------------------
263
+ /** Alem do catalogo do nucleo (`procurarSegredos`): o que a rede nunca deixa sair. */
264
+ const PADROES_DA_REDE = [
265
+ // V8 da revisao 4: token colado a letra ou `_` (`xghp_...`, `backup_sk-proj-...`) tambem vaza, entao
266
+ // nenhum prefixo pede limite a esquerda (X4 da revisao 6). W2 da revisao 5: o que separa o token de
267
+ // uma palavra comum (`task-proj-`, `flask-admin-`, `ASIAPACIFIC...`, `llama2_hf_...`) e a CAUDA de um
268
+ // token de verdade: alfabeto, tamanho, caixa mista e digito. Y2 da revisao 7: o AWS pede borda de
269
+ // maiuscula e digito dos dois lados (colado a minuscula ou `_` continua pego), e a OpenAI, 60 na cauda.
270
+ { nome: 'token do GitHub', regex: /(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})/ },
271
+ { nome: 'token do GitLab', regex: /gl(?:pat|dt|oas|rt|cbt|ptt|ft|imt|agent|soat)-[A-Za-z0-9_-]{20,}/ },
272
+ { nome: 'token do Slack', regex: /xox[abprs]-[A-Za-z0-9-]{10,}/ },
273
+ { nome: 'chave de acesso AWS', regex: /(?<![A-Z0-9])(?:AKIA|ASIA)[A-Z2-7]{16}(?![A-Z0-9])/ },
274
+ { nome: 'token do Hugging Face', regex: /hf_(?=[A-Za-z0-9]*[A-Z])(?=[A-Za-z0-9]*[a-z])[A-Za-z0-9]{34,}/ },
275
+ // A chave de verdade traz `T3BlbkFJ` ("OpenAI" em base64) no corpo; sem ele, so cauda longa de token.
276
+ { nome: 'chave da OpenAI', regex: /sk-(?:proj|svcacct|admin|None)-(?:(?=[A-Za-z0-9_-]*T3BlbkFJ)[A-Za-z0-9_-]{20,}|(?=[A-Za-z0-9_-]*[0-9])(?=[A-Za-z0-9_-]*[A-Z])[A-Za-z0-9_-]{60,})/ },
277
+ { nome: 'token do npm', regex: /npm_[A-Za-z0-9]{36,}/ },
278
+ { nome: 'chave de API do Google', regex: /AIza[0-9A-Za-z_-]{35}/ },
279
+ { nome: 'token JWT', regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./ },
280
+ { nome: 'credencial em URL', regex: /[a-z][a-z0-9+.-]*:\/\/[^\s"]*?[^/\s:@"\\]+:[^/\s@"]*@/i },
281
+ { nome: 'arquivo de credencial', regex: /(?:\.credentials\.json|auth\.json|hosts\.ya?ml|\.git-credentials|\.netrc|\.npmrc|\.pypirc|id_(?:rsa|ed25519|ecdsa)\b|[/\\]\.ssh[/\\]|\.config[/\\](?:gh|glab-cli)\b)/i },
282
+ { nome: 'e-mail de conta', regex: /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b(?!:)/ },
283
+ ];
284
+ /** O nome do padrao de segredo que casa no texto, ou `null`: o catalogo do nucleo e o da rede. */
285
+ function achadoDeSegredo(texto) {
286
+ return (0, policies_1.procurarSegredos)(texto)[0]?.nome ?? PADROES_DA_REDE.find((p) => p.regex.test(texto))?.nome ?? null;
287
+ }
288
+ const CAMPOS = {
289
+ retrato: ['contrato', 'maquina', 'id', 'hostname', 'adesao', 'forjas', 'runtimes', 'hosts', 'projetos', 'versaoOrk', 'publicadoEm'],
290
+ forjas: ['forja', 'host', 'cli', 'versao', 'usuario'],
291
+ runtimes: ['runtime', 'binario', 'versao'],
292
+ hosts: ['host', 'versao', 'adaptador'],
293
+ projetos: ['nome', 'remoto', 'caminho'],
294
+ };
295
+ function exigirSoCampos(onde, v, permitidos) {
296
+ if (!v || typeof v !== 'object' || Array.isArray(v))
297
+ throw new Error(`rede.segredo: ${onde} fora do contrato; nada foi publicado`);
298
+ for (const [k, x] of Object.entries(v)) {
299
+ if (!permitidos.includes(k))
300
+ throw new Error(`rede.segredo: campo "${onde}.${k}" fora da lista de permissao; nada foi publicado`);
301
+ if (x !== null && typeof x === 'string' && x.length > 1024)
302
+ throw new Error(`rede.segredo: campo "${onde}.${k}" longo demais; nada foi publicado`);
303
+ }
304
+ }
305
+ /**
306
+ * D5: o retrato so tem os campos da lista de permissao, e nenhum valor com cara de segredo. O erro
307
+ * diz o padrao e o campo, nunca o valor: relatorio que imprime o segredo vaza de novo.
308
+ */
309
+ function exigirRetratoSeguro(r) {
310
+ exigirSoCampos('retrato', r, CAMPOS.retrato);
311
+ for (const lista of ['forjas', 'runtimes', 'hosts', 'projetos']) {
312
+ if (!Array.isArray(r[lista]))
313
+ throw new Error(`rede.segredo: ${lista} fora do contrato; nada foi publicado`);
314
+ r[lista].forEach((item, i) => exigirSoCampos(`${lista}[${i}]`, item, CAMPOS[lista]));
315
+ }
316
+ // S2 da revisao 2: o que o escritor publica, o leitor aceita; senao, ninguem ve esta maquina.
317
+ if (!normalizarRetrato(r))
318
+ throw new Error('rede.contrato: o retrato desta maquina nao passa no contrato ork.rede-maquina/v1; nada foi publicado');
319
+ const achado = segredoNoRetrato(r);
320
+ if (achado)
321
+ throw new Error(`rede.segredo: padrao "${achado.padrao}" em ${achado.onde}; nada foi publicado (valor omitido de proposito)`);
322
+ }
323
+ /** O primeiro valor do retrato com cara de segredo: o padrao e o campo, nunca o valor. */
324
+ function segredoNoRetrato(r) {
325
+ const visitar = (v, onde) => {
326
+ if (typeof v === 'string') {
327
+ const padrao = achadoDeSegredo(v);
328
+ return padrao ? { padrao, onde } : null;
329
+ }
330
+ if (Array.isArray(v)) {
331
+ for (let i = 0; i < v.length; i++) {
332
+ const a = visitar(v[i], `${onde}[${i}]`);
333
+ if (a)
334
+ return a;
335
+ }
336
+ return null;
337
+ }
338
+ if (v && typeof v === 'object') {
339
+ for (const [k, x] of Object.entries(v)) {
340
+ const a = visitar(x, onde ? `${onde}.${k}` : k);
341
+ if (a)
342
+ return a;
343
+ }
344
+ }
345
+ return null;
346
+ };
347
+ return visitar(r, '');
348
+ }
349
+ /** D6: uma publicacao desta maquina so toca o proprio retrato e o indice. */
350
+ function exigirSoOProprioRetrato(maquina, mudancas) {
351
+ const proprio = arquivoDoRetrato(maquina);
352
+ for (const m of mudancas) {
353
+ if (m.caminho !== proprio && m.caminho !== exports.PAINEL_DA_REDE) {
354
+ throw new Error(`rede.retrato-alheio: ${maquina} tentou gravar ${m.caminho}; cada maquina so escreve ${proprio}`);
355
+ }
356
+ }
357
+ }
358
+ const ehObjeto = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
359
+ // V2 da revisao 4: texto com caractere que o terminal executa ou que ninguem ve fica fora do contrato.
360
+ const ehTexto = (v, teto = 1024) => typeof v === 'string' && v.length <= teto && !rede_projetos_1.INVISIVEL.test(v);
361
+ const ehTextoOuNulo = (v, teto = 1024) => v === null || ehTexto(v, teto);
362
+ /** O teto de itens de cada lista do retrato, igual no escritor e no leitor. */
363
+ exports.MAXIMO_DE_ITENS = 200;
364
+ const PULAR = Symbol('pular');
365
+ /**
366
+ * Cada item de uma lista do retrato, campo a campo; so os campos do contrato saem. Item com valor de
367
+ * enum que esta versao nao conhece (um host novo, outra forja) e PULADO: versao nova e compativel,
368
+ * e a maquina que atualizou primeiro nao some para quem nao atualizou (S2 da revisao 2).
369
+ */
370
+ function itens(v, item) {
371
+ if (!Array.isArray(v) || v.length > exports.MAXIMO_DE_ITENS)
372
+ return null;
373
+ const saida = [];
374
+ for (const x of v) {
375
+ const ok = ehObjeto(x) ? item(x) : null;
376
+ if (ok === null)
377
+ return null;
378
+ if (ok !== PULAR)
379
+ saida.push(ok);
380
+ }
381
+ return saida;
382
+ }
383
+ /**
384
+ * O retrato lido da casa, conferido por inteiro (M1 do CHECK 1): cada campo do contrato com o tipo
385
+ * certo, cada item de cada lista idem. Campo desconhecido e ignorado (versao nova e compativel);
386
+ * campo do contrato ausente ou com outro tipo invalida o arquivo, que vira lacuna. Antes, um
387
+ * `projetos: [null]` derrubava o status, o publicar e o sair de todas as maquinas.
388
+ */
389
+ function normalizarRetrato(bruto) {
390
+ if (!ehObjeto(bruto) || bruto.contrato !== exports.CONTRATO_DO_RETRATO)
391
+ return null;
392
+ const r = bruto;
393
+ if (!ehTexto(r.maquina, 64) || !r.maquina.trim() || !ehTexto(r.hostname, 253) || !ehTexto(r.versaoOrk, 40))
394
+ return null;
395
+ if (!ehTexto(r.publicadoEm, 40) || !Number.isFinite(Date.parse(r.publicadoEm)))
396
+ return null;
397
+ if (r.adesao !== 'rede' && r.adesao !== 'fabrica')
398
+ return null;
399
+ const forjas = itens(r.forjas, (x) => !(ehTexto(x.forja, 40) && ehTexto(x.host, 255) && ehTexto(x.cli, 16) &&
400
+ ehTextoOuNulo(x.versao, 40) && ehTextoOuNulo(x.usuario, 255)) ? null
401
+ : x.forja !== 'github' && x.forja !== 'gitlab' ? PULAR
402
+ : { forja: x.forja, host: x.host, cli: x.cli, versao: x.versao, usuario: x.usuario });
403
+ const runtimes = itens(r.runtimes, (x) => ehTexto(x.runtime, 40) && ehTexto(x.binario, 40) && ehTextoOuNulo(x.versao, 40)
404
+ ? { runtime: x.runtime, binario: x.binario, versao: x.versao } : null);
405
+ const hosts = itens(r.hosts, (x) => !(ehTexto(x.host, 40) && ehTextoOuNulo(x.versao, 40) && ehTextoOuNulo(x.adaptador, 40)) ? null
406
+ : !hosts_1.ORDEM_DOS_HOSTS.includes(x.host) ? PULAR
407
+ : { host: x.host, versao: x.versao, adaptador: x.adaptador });
408
+ const projetos = itens(r.projetos, projetoNoContrato);
409
+ if (!forjas || !runtimes || !hosts || !projetos)
410
+ return null;
411
+ if (r.id !== undefined && !(0, rede_adesao_1.ehIdDeMaquina)(r.id))
412
+ return null;
413
+ return { contrato: exports.CONTRATO_DO_RETRATO, maquina: r.maquina, ...(r.id !== undefined ? { id: r.id } : {}), hostname: r.hostname,
414
+ adesao: r.adesao, forjas, runtimes, hosts, projetos, versaoOrk: r.versaoOrk, publicadoEm: r.publicadoEm };
415
+ }
416
+ /**
417
+ * W1 da revisao 5: o item de projeto do contrato, o MESMO predicado no leitor e no filtro do escritor
418
+ * (antes, a regra de nome do escritor aceitava o que este recusava, e a publicacao inteira caia).
419
+ * V9 da revisao 4: o remoto e remontado; o que nao sai igual (de outra versao ou de outra mao) vira `null`.
420
+ */
421
+ function projetoNoContrato(x) {
422
+ if (!ehTexto(x.nome, 80) || !ehTextoOuNulo(x.remoto, 500) || !ehTexto(x.caminho, 1024))
423
+ return null;
424
+ return { nome: x.nome, remoto: x.remoto !== null && (0, rede_projetos_1.limparRemoto)(x.remoto) === x.remoto ? x.remoto : null, caminho: x.caminho };
425
+ }
426
+ /**
427
+ * Os retratos de uma ponta, cada um conferido: contrato, forma e o NOME do arquivo igual ao da
428
+ * maquina que o retrato diz ser. Arquivo ruim vira `invalidos`, nunca derruba a leitura.
429
+ */
430
+ function retratosDaPonta(cache, ponta) {
431
+ return (0, rede_forja_1.comGitIsolado)(() => lerRetratos(cache, ponta));
432
+ }
433
+ function lerRetratos(cache, ponta) {
434
+ if (!ponta)
435
+ return { retratos: [], invalidos: [] };
436
+ // B12: `-z`, porque sem ele o git poe aspas em nome nao ASCII e o arquivo sumia calado.
437
+ const arquivos = (0, branch_de_estado_1.exigirGit)(cache, ['ls-tree', '-r', '-z', '--name-only', ponta, '--', `${exports.DIR_DOS_RETRATOS}/`], PREFIXO)
438
+ .split('\0').filter(Boolean);
439
+ const retratos = [], invalidos = [];
440
+ for (const arquivo of arquivos) {
441
+ const visivel = JSON.stringify(arquivo).slice(1, -1).slice(0, 120);
442
+ if (!/^maquinas\/[A-Za-z0-9._-]{1,64}\.json$/.test(arquivo)) {
443
+ invalidos.push({ arquivo: visivel, motivo: 'nome de arquivo fora do padrao maquinas/<maquina>.json' });
444
+ continue;
445
+ }
446
+ let bruto;
447
+ try {
448
+ bruto = JSON.parse((0, branch_de_estado_1.exigirGit)(cache, ['show', `${ponta}:${arquivo}`], PREFIXO));
449
+ }
450
+ catch {
451
+ invalidos.push({ arquivo, motivo: 'JSON ilegivel' });
452
+ continue;
453
+ }
454
+ const retrato = normalizarRetrato(bruto);
455
+ if (!retrato) {
456
+ invalidos.push({ arquivo, motivo: `fora do contrato ${exports.CONTRATO_DO_RETRATO}` });
457
+ continue;
458
+ }
459
+ let esperado = null;
460
+ try {
461
+ esperado = arquivoDoRetrato(retrato.maquina);
462
+ }
463
+ catch { /* nome impossivel */ }
464
+ if (esperado !== arquivo) {
465
+ invalidos.push({ arquivo, motivo: `diz ser a maquina "${retrato.maquina}", que nao e a dona deste arquivo` });
466
+ continue;
467
+ }
468
+ // V9 da revisao 4: a mesma varredura do escritor; o que ela pega nao vai ao status nem ao REDE.md.
469
+ // W3 da revisao 5: o projeto com cara de segredo sai sozinho (o escritor faz o mesmo); so campo do
470
+ // nucleo com cara de segredo tira a maquina inteira.
471
+ const achados = retrato.projetos.map((x) => segredoNoRetrato(x));
472
+ const projetos = retrato.projetos.filter((_x, i) => !achados[i]);
473
+ const primeiro = achados.find((x) => x !== null);
474
+ if (primeiro) {
475
+ invalidos.push({ arquivo, motivo: `${retrato.projetos.length - projetos.length} projeto(s) fora da leitura: padrao "${primeiro.padrao}" (valor omitido de proposito)` });
476
+ }
477
+ const lido = { ...retrato, projetos };
478
+ const segredo = segredoNoRetrato(lido);
479
+ if (segredo) {
480
+ invalidos.push({ arquivo, motivo: `padrao "${segredo.padrao}" em ${segredo.onde} (valor omitido de proposito)` });
481
+ continue;
482
+ }
483
+ retratos.push(lido);
484
+ }
485
+ retratos.sort((a, b) => a.maquina.localeCompare(b.maquina));
486
+ return { retratos, invalidos };
487
+ }
488
+ // ---------------------------------------------------------------------------
489
+ // O cache da casa e o git sem prompt.
490
+ // ---------------------------------------------------------------------------
491
+ function dirDoCache(casa) {
492
+ return path.join((0, rede_adesao_1.pastaDaRede)(), `${casa.forja}-${casa.host}-${casa.dono}-${casa.repositorio}`.replace(/[^A-Za-z0-9._-]+/g, '-'));
493
+ }
494
+ /** O cache ja foi criado? */
495
+ const cachePronto = (dir) => fs.existsSync(path.join(dir, '.git', 'HEAD'));
496
+ exports.cachePronto = cachePronto;
497
+ /**
498
+ * O cache da casa: um repositorio com arvore de trabalho VAZIA (nunca ha checkout), porque a
499
+ * remocao de arquivo do `gravarNaBranch` (`update-index --force-remove`) exige arvore de trabalho,
500
+ * como nos clones em que a fabrica roda. Remoto `origin` na URL que a forja devolveu; autor = esta
501
+ * maquina; sem hook nem assinatura (roda em segundo plano); e, para HTTPS, o helper de credencial da
502
+ * propria forja so aqui dentro (D4): o primeiro valor vazio zera os helpers da configuracao global.
503
+ */
504
+ function prepararCache(casa, url, helper, maquina) {
505
+ return (0, rede_forja_1.comGitIsolado)(() => configurarCache(casa, url, helper, maquina));
506
+ }
507
+ function configurarCache(casa, url, helper, maquina) {
508
+ const dir = dirDoCache(casa);
509
+ if (!(0, exports.cachePronto)(dir)) {
510
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
511
+ (0, branch_de_estado_1.exigirGit)(dir, ['init', '--quiet'], PREFIXO);
512
+ }
513
+ const semHooks = path.join(dir, '.git', 'sem-hooks');
514
+ fs.mkdirSync(semHooks, { recursive: true });
515
+ // B6: so grava o que mudou; leitura e publicacao simultaneas nao disputam a config a cada chamada.
516
+ // So a config LOCAL do cache conta: a global da pessoa (o `gh auth setup-git`, por exemplo) fica como esta.
517
+ const config = (chave, valor) => {
518
+ if ((0, branch_de_estado_1.git)(dir, ['config', '--local', '--get', chave]).stdout.replace(/\n$/, '') !== valor)
519
+ (0, branch_de_estado_1.exigirGit)(dir, ['config', '--local', chave, valor], PREFIXO);
520
+ };
521
+ config('remote.origin.url', url);
522
+ config('core.hooksPath', semHooks);
523
+ config('commit.gpgsign', 'false');
524
+ config('user.name', identidadeDoGit(maquina).nome);
525
+ config('user.email', identidadeDoGit(maquina).email);
526
+ if (helper && /^https:\/\//i.test(url)) {
527
+ const chave = `credential.${new URL(url).origin}.helper`;
528
+ if ((0, branch_de_estado_1.git)(dir, ['config', '--local', '--get-all', chave]).stdout !== `\n${helper}\n`) {
529
+ (0, branch_de_estado_1.git)(dir, ['config', '--local', '--unset-all', chave]);
530
+ (0, branch_de_estado_1.exigirGit)(dir, ['config', '--local', '--add', chave, ''], PREFIXO);
531
+ (0, branch_de_estado_1.exigirGit)(dir, ['config', '--local', '--add', chave, helper], PREFIXO);
532
+ }
533
+ }
534
+ return dir;
535
+ }
536
+ /**
537
+ * V9 da revisao 4: a URL da casa e https, com o helper da forja; `http://` mandaria o token sem TLS. O
538
+ * caminho local (sem esquema) e o remoto bare de um espelho ou dos testes: nao ha credencial nele.
539
+ */
540
+ function urlDaCasaAceita(url) {
541
+ return /^https:\/\/[^\s@/]+\//i.test(url) || (path.isAbsolute(url) && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(url));
542
+ }
543
+ /** O autor dos commits da casa: a maquina, com e-mail que nao existe (B8). */
544
+ function identidadeDoGit(maquina) {
545
+ return { nome: nomeSeguro(maquina), email: `${nomeSeguro(maquina)}@rede.orkastery.invalid` };
546
+ }
547
+ const arquivoDaMarca = () => path.join((0, rede_adesao_1.pastaDaRede)(), 'publicada.json');
548
+ function lerMarcaDaRede() {
549
+ try {
550
+ return JSON.parse(fs.readFileSync(arquivoDaMarca(), 'utf8'));
551
+ }
552
+ catch {
553
+ return null;
554
+ }
555
+ }
556
+ /**
557
+ * Suspeitas da revisao de 03/10: a marca troca inteira, por `rename`. Escrita direta deixava o
558
+ * `ork network status`, o doctor e o `publicarRede` (que le fora da trava) ver o arquivo vazio ou
559
+ * cortado, e um processo morto entre o truncamento e a escrita deixava a marca ilegivel: a proxima
560
+ * publicacao perdia a reserva D7 dos projetos. Exportada so para o teste.
561
+ */
562
+ function gravarMarca(marca) {
563
+ fs.mkdirSync((0, rede_adesao_1.pastaDaRede)(), { recursive: true });
564
+ const temporario = `${arquivoDaMarca()}.${process.pid}.tmp`;
565
+ try {
566
+ fs.writeFileSync(temporario, JSON.stringify(marca, null, 2) + '\n', { mode: 0o600 });
567
+ fs.renameSync(temporario, arquivoDaMarca());
568
+ }
569
+ catch (e) {
570
+ try {
571
+ fs.rmSync(temporario, { force: true });
572
+ }
573
+ catch { /* o temporario e so deste processo */ }
574
+ throw e;
575
+ }
576
+ }
577
+ /** A casa com o repositorio conferido: existe e e privado (D3). `criar` so no `ork network entrar`. */
578
+ function casaConferida(opcoes) {
579
+ const r = resolverCasa(opcoes);
580
+ if (!r.casa || !r.forja)
581
+ throw new Error(`rede.sem-forja: ${r.motivo?.detalhe ?? 'nenhuma forja utilizavel'}`);
582
+ let repo = r.forja.repositorio(r.casa.dono, r.casa.repositorio);
583
+ let criado = false;
584
+ if (!repo.existe) {
585
+ if (!opcoes.criar)
586
+ throw new Error(`rede.sem-repositorio: ${(0, exports.refDaCasa)(r.casa)} ainda nao existe; ork network entrar o cria, privado`);
587
+ const login = (r.identidades.find((i) => i.forja === r.forja.nome && i.host === r.forja.host) ?? r.forja.identidade()).usuario;
588
+ if (r.casa.dono !== login) {
589
+ throw new Error(`rede.repositorio-alheio: ${(0, exports.refDaCasa)(r.casa)} nao existe e nao e do login ${login}; crie o repositorio privado na forja e rode de novo`);
590
+ }
591
+ repo = r.forja.criarPrivado(r.casa.repositorio, DESCRICAO);
592
+ criado = true;
593
+ }
594
+ if (repo.privado !== true)
595
+ throw new Error(`rede.repositorio-publico: ${(0, exports.refDaCasa)(r.casa)} nao e privado; nada foi publicado`);
596
+ if (!repo.url)
597
+ throw new Error(`rede.forja: ${r.forja.cli} nao devolveu a URL de ${(0, exports.refDaCasa)(r.casa)}`);
598
+ if (!urlDaCasaAceita(repo.url)) {
599
+ throw new Error(`rede.sem-https: ${r.forja.cli} devolveu para ${(0, exports.refDaCasa)(r.casa)} uma URL sem https; a rede so fala com a forja por https, e nada foi publicado`);
600
+ }
601
+ return { casa: r.casa, forja: r.forja, url: repo.url, criado, identidades: r.identidades };
602
+ }
603
+ /**
604
+ * S8 da revisao 2: no GitLab proprio, a forja do ambiente (sem `GITLAB_HOST` no cron) aponta para
605
+ * gitlab.com; o retrato leva a identidade do HOST DA CASA, lida pela forja da casa.
606
+ */
607
+ function identidadesComACasa(identidades, forja) {
608
+ if (identidades.some((i) => i.forja === forja.nome && i.host === forja.host))
609
+ return identidades;
610
+ const ordem = (i) => (i.forja === 'github' ? 0 : 1);
611
+ return [...identidades.filter((i) => i.forja !== forja.nome), forja.identidade()].sort((a, b) => ordem(a) - ordem(b));
612
+ }
613
+ const TRAVA = () => path.join((0, rede_adesao_1.pastaDaRede)(), 'publicar.lock');
614
+ /** Uma trava sem `pid` valido ha mais que isto e orfa: quem a criou caiu entre o `mkdir` e a gravacao do pid. */
615
+ const TRAVA_ORFA_MS = 5 * 60 * 1000;
616
+ /** U5 da revisao 3: sem `pid`, com `pid` vazio ou com lixo, e velha. A de `pid` valido fica com o monitor-lock (vivo ou morto). */
617
+ function travaOrfa(dir = TRAVA()) {
618
+ try {
619
+ if (Date.now() - fs.statSync(dir).mtimeMs <= TRAVA_ORFA_MS)
620
+ return false;
621
+ const pid = fs.existsSync(path.join(dir, 'pid')) ? Number(fs.readFileSync(path.join(dir, 'pid'), 'utf8')) : NaN;
622
+ return !(Number.isInteger(pid) && pid > 0);
623
+ }
624
+ catch {
625
+ return false;
626
+ }
627
+ }
628
+ /**
629
+ * W9 do CHECK 5 (RM-053 fatia 2): a faxina da trava orfa e SERIAL, sob a trava `<trava>.faxina`.
630
+ * Antes, com tres processos e uma orfa, quem tinha julgado "orfa" podia mover a trava VIVA de outro
631
+ * (que entrou depois da orfa sair) e, se um terceiro ja ocupava o lugar, a viva encalhava em
632
+ * `publicar.lock.orfa-*` e o `liberar()` do dono dava erro depois de um push que ja tinha dado certo.
633
+ *
634
+ * Sob a faxina, o julgamento e refeito no lugar antes de mover: so uma faxina tira orfa (a trava com
635
+ * `pid` valido o monitor-lock troca sozinho, e nunca e orfa aqui), entao o que esta no lugar quando a
636
+ * faxina julga e o que ela move. A trava viva nunca sai do lugar. O renomear antes de apagar fica
637
+ * como segunda guarda (V6 da revisao 4): so apaga o que, ja movido, continua orfao.
638
+ *
639
+ * `ocupada`: outra faxina esta em curso; quem chamou espera a trava como sempre. A faxina que caiu
640
+ * entre o `mkdir` e o `pid` (velha, sem `pid` valido) e tirada, e a proxima rodada faz a faxina.
641
+ */
642
+ function tirarTravaOrfa(trava = TRAVA()) {
643
+ const faxina = `${trava}.faxina`;
644
+ const vez = (0, monitor_lock_1.adquirirLockMonitor)(faxina);
645
+ if (!vez.ok) {
646
+ if (!vez.ativo && travaOrfa(faxina))
647
+ fs.rmSync(faxina, { recursive: true, force: true });
648
+ return 'ocupada';
649
+ }
650
+ try {
651
+ if (!fs.existsSync(trava))
652
+ return 'sumiu';
653
+ if (!travaOrfa(trava))
654
+ return 'viva';
655
+ const lixo = `${trava}.orfa-${process.pid}-${Date.now()}`;
656
+ try {
657
+ fs.renameSync(trava, lixo);
658
+ }
659
+ catch {
660
+ return 'sumiu';
661
+ }
662
+ if (travaOrfa(lixo)) {
663
+ fs.rmSync(lixo, { recursive: true, force: true });
664
+ return 'removida';
665
+ }
666
+ try {
667
+ fs.renameSync(lixo, trava);
668
+ }
669
+ catch { /* uma terceira trava ja ocupa o lugar: esta fica para o dono */ }
670
+ return 'viva';
671
+ }
672
+ finally {
673
+ try {
674
+ vez.liberar();
675
+ }
676
+ catch { /* a faxina e desta chamada; sumir com ela nao desfaz o que foi feito */ }
677
+ }
678
+ }
679
+ /**
680
+ * A trava de escrita na casa desta maquina, esperando ate `esperaMs` quando outra escrita esta em
681
+ * curso. S11 da revisao 2: a trava orfa (sem `pid`, velha) sai uma vez, em vez de devolver "ocupado"
682
+ * para sempre; a com `pid` vivo nunca e tirada.
683
+ */
684
+ function travarCasa(esperaMs) {
685
+ fs.mkdirSync((0, rede_adesao_1.pastaDaRede)(), { recursive: true });
686
+ const limite = Date.now() + esperaMs;
687
+ let tirouOrfa = false;
688
+ for (;;) {
689
+ const trava = (0, monitor_lock_1.adquirirLockMonitor)(TRAVA());
690
+ if (trava.ok)
691
+ return trava;
692
+ if (!trava.ativo && !tirouOrfa && travaOrfa()) {
693
+ tirouOrfa = true;
694
+ tirarTravaOrfa();
695
+ continue;
696
+ }
697
+ if (Date.now() >= limite)
698
+ return { ok: false };
699
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
700
+ }
701
+ }
702
+ /**
703
+ * U2 da revisao 3: o `id` gravado no arquivo com o nome desta maquina, quando esta versao nao le o
704
+ * retrato (contrato mais novo, campo que ela recusa). W3 da revisao 5: vale o CABECALHO bem formado,
705
+ * em qualquer versao: `contrato` `ork.rede-maquina/vN`, `maquina` dona deste arquivo e `id` valido.
706
+ * Assim o retrato legitimo de outra instalacao continua prendendo o nome, e lixo com um `id` (V7 da
707
+ * revisao 4) ou a copia de outra maquina com o nome errado nao prendem. `null` sem cabecalho ou `id`.
708
+ */
709
+ function idNoArquivo(cache, ponta, arquivo) {
710
+ if (!ponta)
711
+ return null;
712
+ const r = (0, rede_forja_1.comGitIsolado)(() => (0, branch_de_estado_1.git)(cache, ['show', `${ponta}:${arquivo}`]));
713
+ if (!r.ok)
714
+ return null;
715
+ try {
716
+ const { contrato, maquina, id } = JSON.parse(r.stdout);
717
+ if (typeof contrato !== 'string' || !/^ork\.rede-maquina\/v[1-9][0-9]{0,5}$/.test(contrato) || typeof maquina !== 'string')
718
+ return null;
719
+ return arquivoDoRetrato(maquina) === arquivo && (0, rede_adesao_1.ehIdDeMaquina)(id) ? id : null;
720
+ }
721
+ catch {
722
+ return null;
723
+ }
724
+ }
725
+ /**
726
+ * Grava o retrato na casa, sob a trava desta maquina. B6: a adesao e reconferida DENTRO da trava
727
+ * (um `sair` no meio nao ve o retrato reaparecer). B7: o arquivo com este nome que for de outra
728
+ * instalacao (outro `id`) nao e regravado, salvo `tomarNome`.
729
+ */
730
+ function gravarNaCasa(conferida, retrato, descartados, opcoes) {
731
+ const { casa, forja, url } = conferida;
732
+ const maquina = retrato.maquina;
733
+ const trava = travarCasa(0);
734
+ if (!trava.ok)
735
+ return { acao: 'ocupado', maquina, casa: (0, exports.refDaCasa)(casa), commit: null, tentativas: 0, descartados };
736
+ try {
737
+ if (opcoes.exigirAdesao && !(0, rede_adesao_1.adesaoDaRede)().membro)
738
+ throw new Error('rede.fora: esta maquina saiu da rede; nada foi publicado');
739
+ const cache = prepararCache(casa, url, forja.helperDeCredencial(), maquina);
740
+ const assinatura = assinaturaDoRetrato(retrato);
741
+ for (let tentativa = 1; tentativa <= TENTATIVAS; tentativa++) {
742
+ const { ponta, atualizado } = (0, rede_forja_1.comGitIsolado)(() => (0, branch_de_estado_1.buscarBranch)(cache, 'origin', exports.BRANCH_DA_REDE, PREFIXO, 30000));
743
+ if (!atualizado)
744
+ throw new Error(`rede.sem-leitura: nao consegui ler ${(0, exports.refDaCasa)(casa)}; publicar exige rede`);
745
+ const { retratos } = retratosDaPonta(cache, ponta);
746
+ const atual = retratos.find((r) => r.maquina === maquina);
747
+ // U2: o arquivo que esta versao nao le ainda pode ser de outra instalacao (contrato mais novo).
748
+ const idDoOutro = atual ? atual.id ?? null : idNoArquivo(cache, ponta, arquivoDoRetrato(maquina));
749
+ const alheio = !!idDoOutro && idDoOutro !== retrato.id;
750
+ if (alheio && !opcoes.tomarNome) {
751
+ // U7 da revisao 3: o hostname igual sugere a propria maquina, mas duas VMs podem se chamar `ubuntu`.
752
+ const dica = atual?.hostname === hostnameSeguro()
753
+ ? '; o retrato tem o hostname desta maquina: se for ela mesma, com ~/.orkastery apagada ou copiada, retome o nome com ' +
754
+ 'ork network entrar --forcar; se for outra maquina ou instalacao com o mesmo hostname, escolha outro nome com --maquina NOME'
755
+ : '; escolha outro nome com ork network entrar --maquina NOME, ou tome este com --forcar';
756
+ const quem = atual ? ` (hostname ${atual.hostname}, batida ${(0, horario_1.formatarDataHora)(atual.publicadoEm)})` : ' (num retrato que esta versao nao le)';
757
+ throw new Error(`rede.nome-em-uso: outra instalacao ja publica como "${maquina}"${quem}${dica}`);
758
+ }
759
+ const mudancas = [
760
+ { caminho: arquivoDoRetrato(maquina), conteudo: JSON.stringify(retrato, null, 2) + '\n' },
761
+ { caminho: exports.PAINEL_DA_REDE, conteudo: painelDaRede([...retratos.filter((r) => r.maquina !== maquina), retrato], Date.parse(retrato.publicadoEm)) },
762
+ ];
763
+ exigirSoOProprioRetrato(maquina, mudancas);
764
+ const mensagem = alheio ? `rede: ${maquina} publicou o retrato, tomando o nome de outra instalacao` : `rede: ${maquina} publicou o retrato`;
765
+ const commit = (0, rede_forja_1.comGitIsolado)(() => (0, branch_de_estado_1.gravarNaBranch)(cache, 'origin', exports.BRANCH_DA_REDE, ponta, mudancas, mensagem, PREFIXO), identidadeDoGit(maquina));
766
+ if (commit) {
767
+ gravarMarca({ assinatura, em: retrato.publicadoEm, commit, casa: (0, exports.refDaCasa)(casa), forja: casa.forja, maquina,
768
+ projetos: retrato.projetos, forjas: retrato.forjas });
769
+ return { acao: 'publicou', maquina, casa: (0, exports.refDaCasa)(casa), commit, tentativas: tentativa, descartados, ...(alheio ? { tomouNome: true } : {}) };
770
+ }
771
+ }
772
+ throw new Error(`rede.concorrencia: ${TENTATIVAS} pushes recusados seguidos; tente de novo em instantes`);
773
+ }
774
+ finally {
775
+ trava.liberar();
776
+ }
777
+ }
778
+ /**
779
+ * `ork network publicar`: grava o retrato desta maquina na casa. So membro publica. M4 do CHECK 1:
780
+ * retrato igual ao ultimo, dentro da batida, nao chama a forja nenhuma vez (as forjas vem da marca);
781
+ * `forcar` publica mesmo assim.
782
+ */
783
+ function publicarRede(opcoes = {}) {
784
+ const adesao = (0, rede_adesao_1.adesaoDaRede)();
785
+ if (!adesao.membro)
786
+ throw new Error('rede.fora: esta maquina nao esta na rede; ork network entrar');
787
+ const amb = opcoes.amb ?? {};
788
+ const maquina = nomeSeguro((0, maquina_1.nomeDaMaquina)(opcoes.maquina));
789
+ const id = (0, rede_adesao_1.idDaMaquina)();
790
+ const agora = opcoes.agora ?? new Date().toISOString();
791
+ const marca = lerMarcaDaRede();
792
+ const base = { amb, maquina, id, diretorio: opcoes.diretorio, agora, arquivoDeProjetos: opcoes.arquivoDeProjetos, adesao: adesao.adesao ?? 'rede' };
793
+ // S15 da revisao 2: relogio que voltou (agora antes da marca) nao conta como dentro da batida.
794
+ const desde = marca ? Date.parse(agora) - Date.parse(marca.em) : NaN;
795
+ const dentroDaBatida = !!marca && marca.maquina === maquina && desde >= 0 && desde < exports.PULSACAO_DA_REDE_MS;
796
+ if (!opcoes.forcar && dentroDaBatida && marca.forjas && !opcoes.forja && !opcoes.repositorio) {
797
+ const barato = retratoComDescartes({ ...base, identidades: marca.forjas, anteriores: marca.projetos });
798
+ if (assinaturaDoRetrato(barato.retrato) === marca.assinatura) {
799
+ return { acao: 'sem-mudanca', maquina, casa: marca.casa, commit: marca.commit, tentativas: 0, descartados: barato.descartados };
800
+ }
801
+ }
802
+ const lidas = opcoes.identidades ?? (0, rede_forja_1.forjasDaMaquina)(amb).map((f) => f.identidade());
803
+ const conferida = casaConferida({ ...opcoes, identidades: lidas });
804
+ const identidades = identidadesComACasa(lidas, conferida.forja);
805
+ const mesmaCasa = marca?.casa === (0, exports.refDaCasa)(conferida.casa) && marca?.maquina === maquina;
806
+ const { retrato, descartados } = retratoComDescartes({ ...base, identidades, anteriores: mesmaCasa ? marca?.projetos : undefined });
807
+ exigirRetratoSeguro(retrato);
808
+ if (!opcoes.forcar && mesmaCasa && dentroDaBatida && marca?.assinatura === assinaturaDoRetrato(retrato)) {
809
+ return { acao: 'sem-mudanca', maquina, casa: marca.casa, commit: marca.commit, tentativas: 0, descartados };
810
+ }
811
+ return gravarNaCasa(conferida, retrato, descartados, { exigirAdesao: true, tomarNome: false });
812
+ }
813
+ /**
814
+ * D9: a batida do pulse (e o `ork network publicar --silencioso` dos eventos). So membro, so com a
815
+ * publicacao ligada, no maximo uma tentativa a cada 14 minutos. `null` quando nao era a vez.
816
+ */
817
+ function publicarRedeNaBatida(opcoes = {}) {
818
+ if ((0, rede_adesao_1.publicacaoDesligada)(opcoes.amb?.env ?? process.env) || !(0, rede_adesao_1.adesaoDaRede)().membro)
819
+ return null;
820
+ if (!(0, rede_adesao_1.tomarVezDePublicar)(opcoes.agoraMs))
821
+ return null;
822
+ return publicarRede(opcoes);
823
+ }
824
+ const NOME_DE_MAQUINA = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
825
+ /**
826
+ * `ork network entrar`. M2 do CHECK 1: atomico do ponto de vista da maquina. Confere (e cria,
827
+ * privada) a casa, publica o primeiro retrato e SO ENTAO grava o nome e a adesao; se a publicacao
828
+ * falha, nada muda aqui. M3: o nome que vale e gravado, como no `ork fabrica entrar`: `--maquina`,
829
+ * senao o ja gravado, senao `ORK_MAQUINA` ou o hostname; assim o cron publica com o mesmo nome.
830
+ */
831
+ function entrarNaRede(opcoes = {}) {
832
+ // O nome explicito tem de ser valido; o que vem do arquivo, de ORK_MAQUINA ou do hostname e saneado.
833
+ const explicito = (opcoes.maquina ?? '').trim();
834
+ const maquina = explicito || nomeSeguro((0, maquina_1.lerConfigDaMaquina)()?.nome || (0, maquina_1.nomeDaMaquina)());
835
+ if (!NOME_DE_MAQUINA.test(maquina)) {
836
+ throw new Error(`rede.maquina: nome de maquina invalido ("${maquina}"); use ork network entrar --maquina NOME (letras, numeros, ponto, _ ou -)`);
837
+ }
838
+ const amb = opcoes.amb ?? {};
839
+ const lidas = opcoes.identidades ?? (0, rede_forja_1.forjasDaMaquina)(amb).map((f) => f.identidade());
840
+ const conferida = casaConferida({ ...opcoes, identidades: lidas, criar: true });
841
+ // S6 da revisao 2: entrar de novo, na mesma casa e com o mesmo nome, mantem a reserva D7 da marca.
842
+ const marca = lerMarcaDaRede();
843
+ const anteriores = marca?.casa === (0, exports.refDaCasa)(conferida.casa) && marca.maquina === maquina ? marca.projetos : undefined;
844
+ const { retrato, descartados } = retratoComDescartes({ amb, maquina, id: (0, rede_adesao_1.idDaMaquina)(), diretorio: opcoes.diretorio,
845
+ agora: opcoes.agora, identidades: identidadesComACasa(lidas, conferida.forja), anteriores, arquivoDeProjetos: opcoes.arquivoDeProjetos,
846
+ adesao: 'rede' });
847
+ exigirRetratoSeguro(retrato);
848
+ const publicacao = gravarNaCasa(conferida, retrato, descartados, { exigirAdesao: false, tomarNome: opcoes.tomarNome });
849
+ if (publicacao.acao === 'ocupado') {
850
+ throw new Error(`rede.ocupado: outra publicacao desta maquina esta em andamento (${TRAVA()}); rode ork network entrar de novo em instantes`);
851
+ }
852
+ (0, maquina_1.gravarConfigDaMaquina)({ nome: maquina });
853
+ const { casa } = conferida;
854
+ const config = (0, rede_adesao_1.gravarConfigDaRede)({ membro: true, forja: casa.forja, host: casa.host, dono: casa.dono, repositorio: casa.repositorio });
855
+ return { config, casa: { ...casa, origem: 'rede.json' }, criado: conferida.criado, publicacao };
856
+ }
857
+ /**
858
+ * `ork network sair`: a adesao vira `membro: false` e o retrato desta maquina sai da casa. B6: sob a
859
+ * trava desta maquina (espera a publicacao em curso). B7: nunca remove o retrato de outra instalacao.
860
+ */
861
+ function sairDaRede(opcoes = {}) {
862
+ // U3 da revisao 3: a saida local vem primeiro; nada depois dela pode deixar a maquina membro.
863
+ (0, rede_adesao_1.gravarConfigDaRede)({ membro: false });
864
+ const maquina = nomeSeguro((0, maquina_1.nomeDaMaquina)(opcoes.maquina));
865
+ const r = resolverCasa(opcoes);
866
+ if (r.casa)
867
+ (0, rede_adesao_1.gravarConfigDaRede)({ forja: r.casa.forja, host: r.casa.host, dono: r.casa.dono, repositorio: r.casa.repositorio });
868
+ try {
869
+ fs.rmSync(arquivoDaMarca(), { force: true });
870
+ }
871
+ catch { /* marca local */ }
872
+ if (!r.casa || !r.forja)
873
+ return { maquina, casa: null, commit: null, alheio: false };
874
+ const repo = r.forja.repositorio(r.casa.dono, r.casa.repositorio);
875
+ if (!repo.existe || !repo.url)
876
+ return { maquina, casa: (0, exports.refDaCasa)(r.casa), commit: null, alheio: false };
877
+ if (!urlDaCasaAceita(repo.url)) {
878
+ throw new Error(`rede.sem-https: saiu da rede aqui, mas ${r.forja.cli} devolveu para ${(0, exports.refDaCasa)(r.casa)} uma URL sem https; o retrato fica na casa`);
879
+ }
880
+ const trava = travarCasa(30000);
881
+ if (!trava.ok)
882
+ throw new Error(`rede.ocupado: saiu da rede aqui, mas outra publicacao desta maquina segura a casa (${TRAVA()}); rode ork network sair de novo`);
883
+ try {
884
+ // Suspeitas da revisao de 03/10: a publicacao que ja segurava a trava quando o `sair` comecou grava
885
+ // a marca ao terminar o push. Sob a trava ela ja terminou, e a proxima reconfere a adesao e recusa
886
+ // (B6): a marca sai de novo aqui e nao volta.
887
+ try {
888
+ fs.rmSync(arquivoDaMarca(), { force: true });
889
+ }
890
+ catch { /* marca local */ }
891
+ const cache = prepararCache(r.casa, repo.url, r.forja.helperDeCredencial(), maquina);
892
+ const proprio = arquivoDoRetrato(maquina);
893
+ const id = (0, rede_adesao_1.idDaMaquina)();
894
+ for (let tentativa = 1; tentativa <= TENTATIVAS; tentativa++) {
895
+ const { ponta, atualizado } = (0, rede_forja_1.comGitIsolado)(() => (0, branch_de_estado_1.buscarBranch)(cache, 'origin', exports.BRANCH_DA_REDE, PREFIXO, 30000));
896
+ if (!atualizado)
897
+ throw new Error(`rede.sem-leitura: saiu da rede aqui, mas nao consegui ler ${(0, exports.refDaCasa)(r.casa)} para tirar o retrato; rode ork network sair de novo com rede`);
898
+ const { retratos, invalidos } = retratosDaPonta(cache, ponta);
899
+ const atual = retratos.find((x) => x.maquina === maquina);
900
+ // S9 da revisao 2: o arquivo com o nome desta maquina sai tambem quando esta invalido; so o de
901
+ // outra instalacao (retrato valido com outro `id`) fica.
902
+ const invalido = invalidos.some((x) => x.arquivo === proprio);
903
+ if (!atual && !invalido)
904
+ return { maquina, casa: (0, exports.refDaCasa)(r.casa), commit: null, alheio: false };
905
+ // U2: invalido para esta versao, mas com o `id` de outra instalacao (contrato mais novo): nao e nosso.
906
+ const idDoOutro = atual ? atual.id ?? null : idNoArquivo(cache, ponta, proprio);
907
+ if (!atual && idDoOutro && idDoOutro !== id)
908
+ return { maquina, casa: (0, exports.refDaCasa)(r.casa), commit: null, alheio: true };
909
+ if (atual?.id && atual.id !== id) {
910
+ return { maquina, casa: (0, exports.refDaCasa)(r.casa), commit: null, alheio: true, ...(atual.hostname === hostnameSeguro() ? { mesmoHostname: true } : {}) };
911
+ }
912
+ const mudancas = [
913
+ { caminho: proprio, conteudo: null },
914
+ { caminho: exports.PAINEL_DA_REDE, conteudo: painelDaRede(retratos.filter((x) => x.maquina !== maquina)) },
915
+ ];
916
+ exigirSoOProprioRetrato(maquina, mudancas);
917
+ const commit = (0, rede_forja_1.comGitIsolado)(() => (0, branch_de_estado_1.gravarNaBranch)(cache, 'origin', exports.BRANCH_DA_REDE, ponta, mudancas, `rede: ${maquina} saiu`, PREFIXO), identidadeDoGit(maquina));
918
+ if (commit)
919
+ return { maquina, casa: (0, exports.refDaCasa)(r.casa), commit, alheio: false };
920
+ }
921
+ throw new Error(`rede.concorrencia: ${TENTATIVAS} pushes recusados seguidos; tente de novo em instantes`);
922
+ }
923
+ finally {
924
+ trava.liberar();
925
+ }
926
+ }
927
+ // ---------------------------------------------------------------------------
928
+ // O REDE.md da casa.
929
+ // ---------------------------------------------------------------------------
930
+ // V2 da revisao 4: a celula nao abre link, imagem, HTML nem codigo, e nao quebra a tabela. W5 da
931
+ // revisao 5 e X1 da revisao 6: o `.` e o `:` escapados desfazem o autolink do GFM (`www.x.y`,
932
+ // `http://x`, mesmo sem ponto no dominio, e-mail), e `&` e `~` nao viram entidade nem riscado; o
933
+ // valor fica numa linha so.
934
+ const celula = (s) => (0, rede_projetos_1.emUmaLinha)(s).replace(/[\\`*_[\]<>|!.:~&]/g, (c) => `\\${c}`);
935
+ /**
936
+ * RM-053 (fatia 2): retrato sem batida ha mais de 14 dias sai do INDICE `REDE.md`. O arquivo dele
937
+ * em `maquinas/` fica (cada maquina so escreve o proprio retrato, D6) e o `ork network status`
938
+ * continua a mostra-lo, com a lacuna `maquina.sem-batida`. Quatorze dias cobrem ferias e uma maquina
939
+ * desligada por duas semanas sem tirar ninguem do indice; o rodape diz quem saiu, nunca em silencio.
940
+ * Batida ilegivel ou no futuro fica no indice: so sai o que se sabe parado.
941
+ */
942
+ exports.RETRATO_PARADO_MS = 14 * 24 * 60 * 60 * 1000;
943
+ /** O retrato publicado ha mais de `RETRATO_PARADO_MS` antes de `agoraMs`. */
944
+ function retratoParado(r, agoraMs) {
945
+ const em = Date.parse(r.publicadoEm);
946
+ return Number.isFinite(em) && agoraMs - em > exports.RETRATO_PARADO_MS;
947
+ }
948
+ /**
949
+ * O `REDE.md`: a mesma rede, para quem abre a forja. Sem caminho local: esses ficam no JSON.
950
+ * `agoraMs` e a hora de quem publica (a batida do proprio retrato), a regua do retrato parado.
951
+ */
952
+ function painelDaRede(retratos, agoraMs = Date.now()) {
953
+ const linhas = [
954
+ '# Orkastery Network',
955
+ '',
956
+ 'As máquinas desta pessoa e o que cada uma tem. Gerado pelo `ork network publicar`; não edite à mão.',
957
+ 'De qualquer máquina: `ork network status`.',
958
+ '',
959
+ '| Máquina | Hostname | Forjas | Runtimes | Hosts | Projetos | ork | Última batida |',
960
+ '| --- | --- | --- | --- | --- | --- | --- | --- |',
961
+ ];
962
+ const ordenados = [...retratos].sort((a, b) => a.maquina.localeCompare(b.maquina));
963
+ const lista = ordenados.filter((r) => !retratoParado(r, agoraMs));
964
+ const parados = ordenados.filter((r) => retratoParado(r, agoraMs));
965
+ if (lista.length === 0)
966
+ linhas.push(`| — | ${parados.length ? 'nenhuma máquina com batida recente' : 'nenhuma máquina publicou'} | — | — | — | — | — | — |`);
967
+ for (const r of lista) {
968
+ const forjas = r.forjas.map((f) => `${f.forja}: ${f.usuario ?? 'sem login'}`).join(', ') || '—';
969
+ const runtimes = r.runtimes.map((x) => `${x.runtime} ${x.versao ?? '?'}`).join(', ') || '—';
970
+ const hosts = r.hosts.map((h) => `${h.host} ${h.versao ?? '?'}${h.adaptador ? ` (adaptador ${h.adaptador})` : ''}`).join(', ') || '—';
971
+ const projetos = r.projetos.map((p) => p.nome).join(', ') || '—';
972
+ linhas.push(`| ${[r.maquina, r.hostname, forjas, runtimes, hosts, projetos, r.versaoOrk, (0, horario_1.formatarDataHora)(r.publicadoEm)].map(celula).join(' | ')} |`);
973
+ }
974
+ if (parados.length) {
975
+ const quem = parados.map((r) => `${celula(r.maquina)} (última batida ${celula((0, horario_1.formatarDataHora)(r.publicadoEm))})`).join(', ');
976
+ linhas.push('', `Fora do índice, sem batida há mais de ${exports.RETRATO_PARADO_MS / 86400000} dias: ${quem}. ` +
977
+ 'O retrato continua em `maquinas/` e no `ork network status`.');
978
+ }
979
+ return [...linhas, '', (0, horario_1.legendaDoFuso)(), ''].join('\n');
980
+ }