@aksp/opencrew 1.6.2 → 1.7.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +70 -13
  3. package/package.json +2 -2
  4. package/src/cli.js +15 -38
  5. package/src/commands/init.js +41 -44
  6. package/src/commands/update.js +78 -75
  7. package/src/lib/blocos.js +148 -0
  8. package/src/lib/deteccao.js +69 -0
  9. package/src/lib/fsx.js +1 -55
  10. package/src/lib/ides.js +4 -0
  11. package/src/lib/legado.js +142 -0
  12. package/src/lib/manifest.js +67 -26
  13. package/src/lib/mcp.js +131 -0
  14. package/src/lib/migrations.js +77 -74
  15. package/src/lib/node-version.js +43 -0
  16. package/src/lib/prompts.js +25 -2
  17. package/src/lib/resumo.js +125 -0
  18. package/templates/.mcp.json +1 -1
  19. package/templates/AGENTS.md +20 -6
  20. package/templates/_opencrew/.opencrew-version +1 -1
  21. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
  22. package/templates/_opencrew/core/escritorio/animacao.js +64 -0
  23. package/templates/_opencrew/core/escritorio/app.js +137 -0
  24. package/templates/_opencrew/core/escritorio/cena.js +132 -0
  25. package/templates/_opencrew/core/escritorio/demo.js +79 -0
  26. package/templates/_opencrew/core/escritorio/escala.js +27 -0
  27. package/templates/_opencrew/core/escritorio/index.html +166 -0
  28. package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
  29. package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
  30. package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
  31. package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
  32. package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
  33. package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
  34. package/templates/_opencrew/core/escritorio/modelo.js +29 -0
  35. package/templates/_opencrew/core/escritorio/painel.js +120 -0
  36. package/templates/_opencrew/core/escritorio/quadro.js +106 -0
  37. package/templates/_opencrew/core/escritorio/rota.js +62 -0
  38. package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
  39. package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
  40. package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
  41. package/templates/_opencrew/core/escritorio/sprites.js +187 -0
  42. package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
  43. package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
  44. package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
  45. package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
  46. package/templates/_opencrew/core/runner.pipeline.md +76 -139
  47. package/templates/_opencrew/core/scripts/comum.mjs +49 -4
  48. package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
  49. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
  50. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
  51. package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
  52. package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
  53. package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
  54. package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
  55. package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
  56. package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
  57. package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
  58. package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
  59. package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
  60. package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
  61. package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
  62. package/templates/_opencrew/core/scripts/estado.mjs +96 -0
  63. package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
  64. package/templates/_opencrew/core/skills.engine.md +7 -3
  65. package/templates/gitignore +1 -0
  66. package/templates/skills/blotato/SKILL.md +39 -10
  67. package/templates/skills/image-ai-generator/SKILL.md +18 -5
  68. package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
  69. package/templates/skills/instagram-publisher/SKILL.md +4 -0
  70. package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
  71. package/templates/skills/resend/SKILL.md +52 -13
@@ -0,0 +1,125 @@
1
+ // What `update` says (spec R2, rules 13 to 16; texts in §6). Each step of the command returns
2
+ // what it created, rewrote or kept, and the lines here are built ONLY from that: no line states
3
+ // something the step did not do, and a step that changed nothing gets no line. These sentences
4
+ // are in PT-BR; the rest of the CLI is not yet (→ U5).
5
+ import path from 'node:path';
6
+ import { copyLine } from './blocos.js';
7
+ import { legacyLines } from './legado.js';
8
+ import { mcpMessage } from './mcp.js';
9
+ import { UNREADABLE } from './manifest.js';
10
+ import { log, info, ok, warn } from './ui.js';
11
+
12
+ /** Last line of `update`: only what the code guarantees (a deleted crew template comes back). */
13
+ export const UNTOUCHED = 'Não foram alterados: as crews que você criou, `_opencrew/_memory/`, `_opencrew/best-practices.local/` e `.env`.';
14
+
15
+ /** `init` in a workspace that is already installed (rule 15). */
16
+ export const ALREADY_INSTALLED = 'Para atualizar, rode `npx @aksp/opencrew@latest update`. Não apague `_opencrew/` para reinstalar: a pasta guarda a sua memória (`_opencrew/_memory/`) e as suas best-practices (`_opencrew/best-practices.local/`).';
17
+
18
+ const NO_BRIDGES = 'Nenhuma ponte de IDE encontrada: nada a atualizar. Para criar a ponte de uma IDE: `npx @aksp/opencrew@latest init --repair-bridges --ide=<id>`.';
19
+ const LEAK_REMOVED = 'CLAUDE.md: removi a seção de STATUS.md que as versões 1.4.0 e 1.4.1 gravaram por engano.';
20
+ const FIRST_PROTECTED = 'Primeira atualização com proteção: sem registro anterior, guardamos tudo o que diferia. Daqui em diante, só o que você editar.';
21
+ // By action of `deliverBlock`; `kept` has no line.
22
+ const BLOCK = {
23
+ 'AGENTS.md': {
24
+ created: 'AGENTS.md criado com o bloco do OpenCrew.',
25
+ added: 'AGENTS.md: bloco do OpenCrew acrescentado no topo; o seu texto foi mantido.',
26
+ updated: 'AGENTS.md: bloco do OpenCrew atualizado.',
27
+ },
28
+ '.gitignore': {
29
+ created: '`.gitignore` criado com o bloco do OpenCrew.',
30
+ added: '`.gitignore`: bloco do OpenCrew acrescentado no fim; as suas linhas foram mantidas.',
31
+ updated: '`.gitignore`: bloco do OpenCrew atualizado.',
32
+ },
33
+ };
34
+ const LEFTOVER = {
35
+ header: (n) => `${n} resto(s) de uma instalação antiga do OpenSquad (a pasta \`_opensquad/\` não existe neste projeto). Nada foi apagado:`,
36
+ bridge: (file) => `${file} — ponte do OpenSquad; o OpenCrew não usa este arquivo.`,
37
+ cites: (file) => `${file} — cita \`_opensquad/\`; confira antes de apagar, pode ser um arquivo seu.`,
38
+ end: 'Se você não usa mais o OpenSquad, pode apagar as pontes listadas. Este aviso volta a cada `update` enquanto os arquivos existirem.',
39
+ config: (file) => `O servidor \`playwright\` do \`.mcp.json\` aponta para \`${file}\`, que não existe neste projeto (resto do OpenSquad). Não alterei o arquivo.`,
40
+ };
41
+ const MCP_REWRITTEN = new Set(['added', 'output-dir']); // actions of `updateMcp` that change the file
42
+ const MAX_COPIES = 15;
43
+
44
+ /** Print the lines a function of this module returned: `[printer, text]` each. */
45
+ export function say(lines) {
46
+ for (const [print, text] of lines) print(text);
47
+ }
48
+
49
+ /** Rule 14: the files `update` wrote in `crews/` because they were missing (`results` of deliverTree). */
50
+ export function recreatedLines(results) {
51
+ const files = results.filter((r) => r.action === 'created').map((r) => r.file);
52
+ if (!files.length) return [];
53
+ return [[info, `${files.length} arquivo(s) que faltava(m) em \`crews/\` foram entregues de novo: ${files.join(', ')}.`]];
54
+ }
55
+
56
+ const blockLines = (results) => results
57
+ .map((r) => BLOCK[r?.file]?.[r.action])
58
+ .filter(Boolean)
59
+ .map((text) => [ok, text]);
60
+
61
+ /** One line per case that happened: IDEs with a bridge rewritten, files created, or neither. */
62
+ function bridgeLines(ides, bridges, legacy = []) {
63
+ if (!ides.length) return [[info, NO_BRIDGES]];
64
+ const cleaned = new Set(legacy.filter((l) => l.action === 'removed').map((l) => l.file));
65
+ const rewritten = bridges.filter((b) => b.action === 'updated' || b.action === 'added' || cleaned.has(b.file));
66
+ const labels = [...new Set(rewritten.map((b) => b.ide.label))];
67
+ const created = bridges.filter((b) => b.action === 'created' && !b.shared).map((b) => b.file);
68
+ const lines = [];
69
+ if (labels.length) lines.push([ok, `Pontes atualizadas: ${labels.join(', ')}.`]);
70
+ if (created.length) lines.push([ok, `Pontes criadas: ${created.join(', ')}.`]);
71
+ return lines.length ? lines : [[ok, 'Pontes das IDEs já estavam em dia.']];
72
+ }
73
+
74
+ function mcpLines(ctx, mcp) {
75
+ const said = mcpMessage(ctx, mcp.action);
76
+ return said ? [[said.warn ? warn : ok, said.text]] : [];
77
+ }
78
+
79
+ /** Rule 16. `mcp` = what this run did to .mcp.json: the line only says "não alterei" if so. */
80
+ function leftoverLines({ files, config }, mcp) {
81
+ const lines = [];
82
+ if (files.length) {
83
+ lines.push([warn, LEFTOVER.header(files.length)]);
84
+ for (const { file, bridge } of files) lines.push([log, ` ${(bridge ? LEFTOVER.bridge : LEFTOVER.cites)(file)}`]);
85
+ lines.push([info, LEFTOVER.end]);
86
+ }
87
+ if (config && !MCP_REWRITTEN.has(mcp.action)) lines.push([warn, LEFTOVER.config(config)]);
88
+ return lines;
89
+ }
90
+
91
+ /** The copies of this run. `unreadable` = the manifest existed and could not be used. */
92
+ function copyLines(ctx, unreadable) {
93
+ const n = ctx.copied.length;
94
+ if (!n) return [];
95
+ const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
96
+ const header = ctx.manifest
97
+ ? `${n} arquivo(s) foram copiados para ${dir}/ antes de serem substituídos (editados por você, ou sem registro de entrega):`
98
+ : `${n} arquivo(s) diferentes do pacote novo foram copiados para ${dir}/ antes de serem substituídos:`;
99
+ const lines = [[warn, header], ...ctx.copied.slice(0, MAX_COPIES).map((f) => [log, ` ${copyLine(ctx, f)}`])];
100
+ if (n > MAX_COPIES) lines.push([log, ` … e mais ${n - MAX_COPIES}`]);
101
+ if (unreadable) lines.push([info, UNREADABLE.summary]);
102
+ else if (!ctx.manifest) lines.push([info, FIRST_PROTECTED]);
103
+ return lines;
104
+ }
105
+
106
+ /**
107
+ * The summary of an `update`, in the order the steps ran.
108
+ * @param {object} ctx delivery context (src/lib/manifest.js)
109
+ * @param {object} done what each step returned: `agents` and `gitignore` (deliverBlock; `agents`
110
+ * is null when a pre-1.3 AGENTS.md was migrated), `ides` (detected), `bridges` (deliverBridges),
111
+ * `leak` (the STATUS.md section left CLAUDE.md's block), `mcp` (updateMcp), `leftovers`
112
+ * (findLeftovers) and `unreadable` (manifest)
113
+ * @returns {Array<[Function, string]>} lines for `say`
114
+ */
115
+ export function updateSummary(ctx, done) {
116
+ return [
117
+ ...blockLines([done.agents, done.gitignore]),
118
+ ...bridgeLines(done.ides, done.bridges, ctx.legacy),
119
+ ...(done.leak ? [[ok, LEAK_REMOVED]] : []),
120
+ ...legacyLines(ctx).map((line) => [warn, line]),
121
+ ...mcpLines(ctx, done.mcp),
122
+ ...leftoverLines(done.leftovers, done.mcp),
123
+ ...copyLines(ctx, done.unreadable),
124
+ ];
125
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "The Playwright MCP version is pinned for stability. To upgrade: npx @playwright/mcp@latest --version, then update the version below and run 'npx @aksp/opencrew update' to refresh.",
2
+ "_comment": "Versão do Playwright MCP fixada por estabilidade. Para trocar, edite a versão abaixo e reinicie a IDE. O `update` do OpenCrew não altera a versão que está neste arquivo.",
3
3
  "mcpServers": {
4
4
  "playwright": {
5
5
  "command": "npx",
@@ -58,6 +58,8 @@ Route input to the matching action:
58
58
  | `/opencrew edit-company` | Re-run company profile setup |
59
59
  | `/opencrew show-company` | Display company.md contents |
60
60
  | `/opencrew settings` | Show/edit preferences.md |
61
+ | `/opencrew dashboard` | Turn on and open the Escritório (live view) — see "Dashboard (Optional)" |
62
+ | `/opencrew dashboard off` | Turn the Escritório off — see "Dashboard (Optional)" |
61
63
  | `/opencrew reset` | Confirm and reset all configuration |
62
64
  | Natural language about crews | Infer intent and route accordingly |
63
65
 
@@ -90,12 +92,24 @@ When running a crew:
90
92
 
91
93
  ## Dashboard (Optional)
92
94
 
93
- The dashboard is an optional animated view of a crew run (`dashboard/index.html`).
94
- It is **disabled by default**; most installs never use it. Toggle it via
95
- `Dashboard: enabled|disabled` in `_opencrew/_memory/preferences.md` (editable via
96
- `/opencrew settings`). When disabled, the runner never writes `state.json`; when
97
- enabled, it writes `crews/{name}/state.json` before each step and at every handoff
98
- (see `_opencrew/core/runner.pipeline.md`).
95
+ The dashboard is the **Escritório**: a local page that shows the crew at work, step by step.
96
+ It is **disabled by default**. The switch is the `Dashboard` line of
97
+ `_opencrew/_memory/preferences.md`; while it says `enabled`, the Pipeline Runner reports each
98
+ step with one short command (see `_opencrew/core/runner.pipeline.md`).
99
+
100
+ **`/opencrew dashboard`** — in this order:
101
+ 1. Write `- **Dashboard:** enabled` in `_opencrew/_memory/preferences.md`, changing only that
102
+ line (if the file has no `Dashboard` line, add it at the end). Leave the rest of the file as is.
103
+ 2. Start `node _opencrew/core/scripts/escritorio.mjs` in the background, from the project root.
104
+ The line it prints carries the address (`http://127.0.0.1:<port>`; the port may vary).
105
+ 3. Show the address and tell the user that the next crew run appears there.
106
+
107
+ If your IDE cannot keep a process running in the background, do step 1 and show the user the
108
+ command of step 2 to run in another terminal. Running `/opencrew dashboard` again is safe: the
109
+ script answers with the same address instead of opening a second page.
110
+
111
+ **`/opencrew dashboard off`** — write `- **Dashboard:** disabled` the same way (only that line)
112
+ and touch nothing else: stop no process, delete no file.
99
113
 
100
114
  ## Language Handling
101
115
 
@@ -1 +1 @@
1
- 1.6.2
1
+ 1.7.0
@@ -11,7 +11,7 @@ version: "1.0.0"
11
11
  ## Compact Rules
12
12
 
13
13
  1. Never publish live content without explicit user confirmation.
14
- 2. Always execute and report a successful dry-run before offering the live publish option.
14
+ 2. With `instagram-publisher`, execute and report a successful dry-run before offering the live publish option; `blotato` has no dry-run: send nothing to it, not even media, before the user answers with the word `publicar`.
15
15
  3. Validate all platform-specific constraints (image format, caption length) before API calls.
16
16
  4. Adapt and format content natively for each target platform; do not cross-post raw text.
17
17
  5. Report publishing results immediately, including success (with URL) or failure details.
@@ -19,7 +19,7 @@ version: "1.0.0"
19
19
  7. Track API usage and proactively warn users if approaching rate limits.
20
20
  8. Fall back gracefully if a required publishing skill is missing; list available alternatives.
21
21
  9. Inform the user and seek permission before converting image formats (e.g., PNG to JPEG).
22
- 10. Display a structured preview (platform, images, caption, hashtags, validations) before dry-run.
22
+ 10. Display a structured preview (platform, images, caption, hashtags, validations) before anything is sent (the `instagram-publisher` dry-run included).
23
23
  11. Do not silently truncate captions; ask the user to shorten them if limits are exceeded.
24
24
  12. Request user direction (continue or abort) if one platform fails in a multi-platform batch.
25
25
 
@@ -29,9 +29,9 @@ version: "1.0.0"
29
29
 
30
30
  ## Core Principles
31
31
 
32
- 1. **Never publish without explicit user confirmation.** This is the cardinal rule. Before any live post, present the full preview (platform, images, caption, hashtags) and wait for the user to confirm. A dry-run is not confirmation. The user must explicitly say "publish" or "go ahead" before any live API call is made.
32
+ 1. **Never publish without explicit user confirmation.** This is the cardinal rule. Before any live post, present the full preview (platform, images, caption, hashtags) and wait for the user to confirm. A dry-run is not confirmation. The user must answer with the confirmation word the publishing skill asks for (e.g. `publicar`) before any live API call is made; any other answer means do not publish.
33
33
 
34
- 2. **Dry-run first, always.** The first execution of any publishing workflow must be a dry-run (test mode). This validates that credentials are configured, images meet requirements, captions are within limits, and the API connection works. Only after a successful dry-run should the user be offered the option to publish for real.
34
+ 2. **Dry-run first, where the skill has one (`instagram-publisher`).** There, the first execution of the publishing workflow must be a dry-run (test mode). This validates that credentials are configured, images meet requirements, captions are within limits, and the API connection works. Only after a successful dry-run should the user be offered the option to publish for real. `blotato` has no dry-run: send nothing to it, not even media, before the user answers with the word `publicar` — its upload is part of the publication.
35
35
 
36
36
  3. **Validate platform requirements before attempting to publish.** Every platform has specific constraints. Validate all of them before making any API call. If validation fails, report the specific issue and suggest a fix before proceeding.
37
37
 
@@ -107,12 +107,12 @@ Every platform has specific constraints that must be validated before making any
107
107
  Status: All validations passed
108
108
  ```
109
109
 
110
- 5. **Execute dry-run.** Run the publishing workflow in test mode:
110
+ 5. **Execute dry-run (`instagram-publisher` only).** Run the publishing workflow in test mode:
111
111
  - Instagram: `--dry-run` flag on the publish script
112
- - Blotato: validate API connection and media upload without posting
113
- - Report dry-run results: credentials OK, media uploaded, container created, ready to publish.
112
+ - Blotato: no dry-run. Skip this step: nothing is uploaded or sent before the confirmation word (step 6)
113
+ - Report the `instagram-publisher` dry-run results: credentials OK, media uploaded, container created, ready to publish.
114
114
 
115
- 6. **Request final confirmation.** Present the dry-run results and ask the user to confirm the live publish. Do not proceed without explicit approval.
115
+ 6. **Request final confirmation.** Present the dry-run results (Blotato: the preview) and ask for the skill's confirmation word. Do not proceed without it.
116
116
 
117
117
  7. **Publish and report.** Execute the live publish. Report the result immediately:
118
118
  - Success: post URL, post ID, platform, timestamp
@@ -130,7 +130,7 @@ Every platform has specific constraints that must be validated before making any
130
130
  ## Quality Criteria
131
131
 
132
132
  - [ ] User confirmation was received before any live publish (not just dry-run)
133
- - [ ] Dry-run was executed and passed before live publish
133
+ - [ ] Dry-run was executed and passed before live publish (`instagram-publisher`); nothing was sent to Blotato before the confirmation word
134
134
  - [ ] All platform-specific validations passed (image format, dimensions, caption length, image count)
135
135
  - [ ] Publish preview was presented with complete details (platform, images, caption, validation status)
136
136
  - [ ] Successful publishes include post URL/permalink and post ID
@@ -213,7 +213,7 @@ Skill: blotato (multi-platform)
213
213
 
214
214
  PLATFORM 1/3: Instagram
215
215
  Validation: All checks passed
216
- Dry-run: Passed
216
+ Preview: Confirmed with the word "publicar"
217
217
  Publish: Published successfully
218
218
  Post URL: https://www.instagram.com/p/DEF456abc/
219
219
  Post ID: ig_17899506834567890
@@ -221,7 +221,7 @@ PLATFORM 1/3: Instagram
221
221
 
222
222
  PLATFORM 2/3: LinkedIn
223
223
  Validation: All checks passed
224
- Dry-run: Passed
224
+ Preview: Confirmed with the word "publicar"
225
225
  Publish: FAILED
226
226
  Error: 403 Forbidden — "Publishing permission not granted"
227
227
  HTTP Status: 403
@@ -245,7 +245,7 @@ PLATFORM 3/3: X/Twitter
245
245
  [User chooses: b, provides short caption]
246
246
 
247
247
  Validation: All checks passed (short caption: 142 chars)
248
- Dry-run: Passed
248
+ Preview: Confirmed with the word "publicar"
249
249
  Publish: Published successfully
250
250
  Post URL: https://x.com/brandname/status/1234567890123456789
251
251
  Post ID: tw_1234567890123456789
@@ -276,7 +276,7 @@ SUMMARY
276
276
 
277
277
  5. **Never report success without a URL.** "Published successfully" without a post URL is not verifiable. Every successful publish must include the post permalink. If the API does not return a URL, report that as a limitation.
278
278
 
279
- 6. **Never assume credentials are valid.** Always verify credentials during the dry-run phase. Tokens expire, permissions get revoked, accounts get disconnected. A credential check is part of every publish workflow.
279
+ 6. **Never assume credentials are valid.** Always verify credentials before publishing (the dry-run with `instagram-publisher`; listing the accounts, read-only, with `blotato`). Tokens expire, permissions get revoked, accounts get disconnected. A credential check is part of every publish workflow.
280
280
 
281
281
  7. **Never publish the same raw caption across all platforms without adaptation.** Instagram, LinkedIn, and X/Twitter have different formatting conventions, character limits, and audience expectations. At minimum, verify the caption fits the platform constraints. Ideally, suggest platform-specific adaptations.
282
282
 
@@ -284,7 +284,7 @@ SUMMARY
284
284
 
285
285
  1. **Present a structured preview before every publish.** Show: platform, account, images (with dimensions and format), caption (with character count), hashtags, and validation status. The user must see exactly what will be published.
286
286
 
287
- 2. **Run a dry-run before every live publish.** Test the full workflow without posting. Verify credentials, upload media, create containers, validate everything. Report dry-run results before requesting confirmation.
287
+ 2. **Run a dry-run before every live publish with `instagram-publisher`.** Test the full workflow without posting. Verify credentials, upload media, create containers, validate everything. Report dry-run results before requesting confirmation. With `blotato` there is no dry-run: the preview and the confirmation word come before any upload.
288
288
 
289
289
  3. **Report results immediately after each publish.** Do not batch results. After each platform publish (success or failure), report the outcome with all relevant details before moving to the next platform.
290
290
 
@@ -0,0 +1,64 @@
1
+ // O que anima (regra 28): a passagem de bastão e a comemoração nascem da diferença entre duas
2
+ // leituras da mesma crew, nunca de um status gravado. `transicao` anota no modelo a hora em que
3
+ // cada uma começou; `quadro` (em quadro.js) desenha pelo tempo decorrido desde então.
4
+ // Puro: sem DOM, sem relógio, sem rede. A hora entra por parâmetro.
5
+ // Spec: fase-e1-escritorio-ao-vivo.md, regra 28 (repositório do OpenCrew).
6
+
7
+ /** A entrega: ida até a frente da mesa de quem recebe, com o papel, e volta até sentar. */
8
+ export const IDA_MS = 2000;
9
+ export const VOLTA_MS = 2000;
10
+ /** Quanto dura a comemoração de uma execução concluída. */
11
+ export const COMEMORACAO_MS = 4000;
12
+ /** Quem digita troca de pose a cada 0,3 s; quem anda, a cada 0,15 s. */
13
+ export const DIGITAR_MS = 300;
14
+ export const ANDAR_MS = 150;
15
+
16
+ /** Nada em curso: é o que vale na primeira leitura de cada crew. */
17
+ export const SEM_ANIMACAO = Object.freeze({ entrega: null, comemoracao: null });
18
+
19
+ /** As duas leituras têm estado e são da mesma crew (ou as duas são da demonstração). */
20
+ function mesmaCrew(anterior, atual) {
21
+ if (!anterior || !atual || anterior.semEstado || atual.semEstado) return false;
22
+ return Boolean(anterior.demo) === Boolean(atual.demo) && anterior.crew === atual.crew;
23
+ }
24
+
25
+ /** A passagem de bastão cuja data a leitura anterior não trazia; senão (ou sem data), `null`. */
26
+ function entregaNova(anterior, atual, agoraMs) {
27
+ const { passagem } = atual;
28
+ if (!passagem || passagem.completedAt === (anterior.passagem?.completedAt ?? '')) return null;
29
+ return { deId: passagem.deId, paraId: passagem.paraId, inicioMs: agoraMs };
30
+ }
31
+
32
+ /** Começa quando a execução passa a `completed`, segue enquanto ela continuar assim. */
33
+ function comemoracaoDe(anterior, atual, agoraMs) {
34
+ if (atual.execucao !== 'completed') return null;
35
+ return anterior.execucao === 'completed' ? (anterior.animacao?.comemoracao ?? null) : { inicioMs: agoraMs };
36
+ }
37
+
38
+ /**
39
+ * Compara duas leituras e devolve a atual com o que está animando. A página chama a cada leitura
40
+ * (cada consulta ao servidor, cada passo da demonstração), e não só quando vai pintar: assim a
41
+ * entrega que acontece com a aba oculta já aparece terminada na volta.
42
+ * @param {object|null} anterior o que esta função devolveu na leitura anterior; `null` na primeira
43
+ * @param {object} atual o modelo da leitura atual (de `montarPagina` ou `montarModelo`)
44
+ * @param {number} agoraMs a hora desta leitura, em milissegundos, no mesmo relógio de `quadro`
45
+ * @returns {object} `atual` mais `animacao`: `{ entrega, comemoracao }` · `entrega`:
46
+ * `{ deId, paraId, inicioMs }` desde que `passagem.completedAt` mudou entre duas leituras da
47
+ * mesma crew, ou `null`; uma passagem nova substitui a que está em curso, e qualquer outra
48
+ * mudança de estado a deixa como está · `comemoracao`: `{ inicioMs }` desde que a execução
49
+ * passou a `completed`, ou `null` · na primeira leitura de uma crew (abrir, trocar de crew,
50
+ * sair da demonstração, voltar de "sem estado"), as duas são `null` · `animacao` só guarda
51
+ * quando cada uma começou, e a entrega fica anotada depois de terminar: quem diz o que ainda
52
+ * se move num instante é `quadro`
53
+ */
54
+ export function transicao(anterior, atual, agoraMs) {
55
+ if (!mesmaCrew(anterior, atual)) return { ...atual, animacao: SEM_ANIMACAO };
56
+ const emCurso = anterior.animacao?.entrega ?? null;
57
+ return {
58
+ ...atual,
59
+ animacao: {
60
+ entrega: entregaNova(anterior, atual, agoraMs) ?? emCurso,
61
+ comemoracao: comemoracaoDe(anterior, atual, agoraMs),
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,137 @@
1
+ // A página do escritório: pergunta o estado ao servidor a cada segundo (e na hora, ao voltar para
2
+ // a aba), entrega a resposta ao modelo e aplica o que ele devolve — o desenho no canvas, os
3
+ // rótulos por cima e o painel em volta. Consulta que falha mantém o último estado bom e liga o
4
+ // aviso; a página segue tentando, sem recarregar.
5
+ // É o único módulo que toca na janela: o relógio, a rede, o endereço (`?demo`), o tamanho da tela
6
+ // e a preferência "reduzir movimento" entram no modelo por parâmetro.
7
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 20 a 23 e 28 (repositório do OpenCrew).
8
+ import { ALTURA, LARGURA, MEMORIA_INICIAL, consultar, emExibicao, escala, escolher, montarPagina, quadro, transicao } from './modelo.js';
9
+ import { animar } from './cena.js';
10
+ import { criarPainel } from './painel.js';
11
+ import { criarRotulos } from './rotulos.js';
12
+
13
+ /** De quanto em quanto tempo a página pergunta o estado. */
14
+ const INTERVALO_MS = 1000;
15
+ /** Consulta sem resposta neste prazo é abandonada e conta como falha: a seguinte sai no ritmo normal. */
16
+ const PRAZO_MS = 4000;
17
+ /** Na demonstração nenhuma consulta traz o passo seguinte: o roteiro é relido neste ritmo. */
18
+ const RITMO_DA_DEMO_MS = 200;
19
+ /** Relativo à página: o servidor só atende o próprio endereço. */
20
+ const ENDERECO = 'estado';
21
+
22
+ /** Regra 16: o canvas tem 320·N × 180·N pixels de verdade e, na página, o tamanho que `escala` devolve. */
23
+ function ajustar(p) {
24
+ const [moldura, sala, tela] = ['moldura', 'sala', 'tela'].map(p.el);
25
+ p.razao = p.janela.devicePixelRatio;
26
+ const { n, largura, altura } = escala(moldura.clientWidth, moldura.clientHeight, p.razao);
27
+ if (tela.width !== LARGURA * n) Object.assign(tela, { width: LARGURA * n, height: ALTURA * n });
28
+ sala.style.width = `${largura}px`;
29
+ sala.style.height = `${altura}px`;
30
+ // Quantos pixels da página vale um pixel lógico: o CSS dos rótulos acompanha a escala por aqui.
31
+ sala.style.setProperty('--u', String(largura / LARGURA));
32
+ }
33
+
34
+ /**
35
+ * Uma leitura: monta o modelo deste instante, compara com a leitura anterior (é daí que saem a
36
+ * entrega e a comemoração) e escreve a página. Na primeira, mostra a página e mede o canvas.
37
+ */
38
+ function ler(p) {
39
+ const agora = Date.now();
40
+ const naDemo = emExibicao(p.memoria, { demo: p.demo }).modo === 'demo';
41
+ p.demoDesde = naDemo ? (p.demoDesde ?? agora) : null;
42
+ const opcoes = { agoraMs: agora, reduzirMovimento: p.menosMovimento.matches, demo: p.demo, decorridoDemoMs: naDemo ? agora - p.demoDesde : 0 };
43
+ const primeira = p.leitura === null;
44
+ p.leitura = transicao(p.leitura, montarPagina(p.memoria, opcoes), agora);
45
+ p.lidaEm = agora;
46
+ p.painel.atualizar(p.leitura);
47
+ p.rotulos.atualizar(p.leitura.agentes);
48
+ if (!primeira) return;
49
+ p.el('pagina').hidden = false;
50
+ ajustar(p);
51
+ }
52
+
53
+ /** A resposta de `/estado`, ou `null` se a consulta falhou, demorou demais ou veio inválida. */
54
+ async function perguntar(p) {
55
+ const corte = new AbortController();
56
+ const prazo = p.janela.setTimeout(() => corte.abort(), PRAZO_MS);
57
+ try {
58
+ const resposta = await p.janela.fetch(ENDERECO, { cache: 'no-store', signal: corte.signal });
59
+ return resposta.ok ? await resposta.json() : null;
60
+ } catch {
61
+ return null;
62
+ } finally {
63
+ p.janela.clearTimeout(prazo);
64
+ }
65
+ }
66
+
67
+ /** Regra 20: uma consulta por vez; a que falha entra na memória como falha, e a página não para. */
68
+ async function consultarServidor(p) {
69
+ if (p.consultando) return;
70
+ p.consultando = true;
71
+ const resposta = await perguntar(p);
72
+ p.consultando = false;
73
+ p.memoria = consultar(p.memoria ?? MEMORIA_INICIAL, resposta);
74
+ ler(p);
75
+ }
76
+
77
+ /** Regra 22: a escolha do usuário no seletor vale até ele trocar. */
78
+ function escolherCrew(p, crew) {
79
+ if (!p.memoria) return;
80
+ p.memoria = escolher(p.memoria, crew);
81
+ ler(p);
82
+ }
83
+
84
+ /** Os bonecos deste instante, para a cena pintar; `null` antes da primeira leitura. */
85
+ function quadroAgora(p) {
86
+ if (!p.leitura) return null;
87
+ const agora = Date.now();
88
+ if (p.leitura.modo === 'demo' && agora - p.lidaEm >= RITMO_DA_DEMO_MS) ler(p);
89
+ if (p.janela.devicePixelRatio !== p.razao) ajustar(p);
90
+ const bonecos = quadro(p.leitura, agora);
91
+ p.rotulos.acompanhar(bonecos);
92
+ return bonecos;
93
+ }
94
+
95
+ /** O que a página guarda entre uma consulta e a seguinte. */
96
+ function criarPagina(janela) {
97
+ const documento = janela.document;
98
+ const el = (id) => documento.getElementById(id);
99
+ const p = {
100
+ janela,
101
+ documento,
102
+ el,
103
+ demo: new URLSearchParams(janela.location.search).has('demo'),
104
+ menosMovimento: janela.matchMedia('(prefers-reduced-motion: reduce)'),
105
+ rotulos: criarRotulos(el('rotulos'), documento),
106
+ memoria: null, // as consultas ao servidor; `null` até a primeira voltar
107
+ leitura: null, // o modelo da última leitura, com o que está animando
108
+ lidaEm: 0,
109
+ demoDesde: null, // quando a demonstração entrou na tela
110
+ consultando: false,
111
+ razao: 0, // a razão de pixels com que o canvas foi medido
112
+ };
113
+ p.painel = criarPainel(documento, (crew) => escolherCrew(p, crew));
114
+ return p;
115
+ }
116
+
117
+ /**
118
+ * Liga a página a uma janela. A frase de abertura, que já está no HTML, só some depois do
119
+ * primeiro desenho: se algo aqui não carregar ou não rodar, ela continua na tela.
120
+ * @param {Window} janela a janela do navegador (nos testes, uma de mentira)
121
+ */
122
+ export function iniciar(janela) {
123
+ const p = criarPagina(janela);
124
+ const reajustar = () => ajustar(p);
125
+ janela.addEventListener('resize', reajustar);
126
+ if (janela.ResizeObserver) new janela.ResizeObserver(reajustar).observe(p.el('moldura'));
127
+ p.documento.addEventListener('visibilitychange', () => {
128
+ if (!p.documento.hidden) consultarServidor(p);
129
+ });
130
+ janela.setInterval(() => consultarServidor(p), INTERVALO_MS);
131
+ animar(janela, p.el('tela'), () => quadroAgora(p), () => {
132
+ p.el('abertura').hidden = true;
133
+ });
134
+ consultarServidor(p);
135
+ }
136
+
137
+ if (typeof window !== 'undefined') iniciar(window);
@@ -0,0 +1,132 @@
1
+ // A cena: pinta no canvas, em pixel lógico (320×180), o quadro que o modelo calculou — a sala, a
2
+ // mesa de cada agente do elenco com o monitor, e os bonecos. Nada aqui decide pose, posição ou
3
+ // cor de monitor: isso chega pronto de `quadro`. A cena só preenche retângulos; nenhum texto
4
+ // entra no canvas (nome e balão são elementos da página, numa camada por cima).
5
+ // O laço de animação pede um quadro por vez ao navegador e só pinta de novo quando ele muda.
6
+ // Spec: fase-e1-escritorio-ao-vivo.md, regras 16, 18, 19 e 28 (repositório do OpenCrew).
7
+ import { ALTURA, BONECO, COLUNAS, LARGURA, LARGURA_COLUNA, TOPO } from './modelo.js';
8
+ import { coresDoBoneco, linhasDoBoneco, retangulos } from './sprites.js';
9
+ import { JANELA, LADRILHO, LADRILHO_ESCURO, PAREDE, PLANTA, RELOGIO } from './sprites-sala.js';
10
+ import {
11
+ MESA, MESA_ACIMA, MONITOR, MONITOR_EM, SINAL_DUVIDA, SINAL_FALHA, SINAL_FEITO, SOMBRA,
12
+ TELA_AMBAR, TELA_EM, TELA_VERMELHA, TELAS_ACESAS,
13
+ } from './sprites-mesa.js';
14
+
15
+ /** Quem foi pulado aparece meio transparente. */
16
+ const ALFA_DO_PULADO = 0.45;
17
+ /** O sinal sobre a cabeça, por ação; quem entrega ou comemora não tem sinal. */
18
+ const SINAIS = Object.freeze({ done: SINAL_FEITO, failed: SINAL_FALHA, 'sem-sinal': SINAL_DUVIDA });
19
+ /** A tela do monitor, pela cor que o quadro manda pintar agora; apagado é o próprio monitor. */
20
+ const TELAS = Object.freeze({
21
+ aceso: (fase) => TELAS_ACESAS[fase % TELAS_ACESAS.length],
22
+ ambar: () => TELA_AMBAR,
23
+ vermelho: () => TELA_VERMELHA,
24
+ });
25
+
26
+ const largura = (desenho) => desenho.linhas[0].length;
27
+ const altura = (desenho) => desenho.linhas.length;
28
+ const meio = (coluna) => coluna * LARGURA_COLUNA + LARGURA_COLUNA / 2;
29
+ const centrado = (desenho, x, y) => ({ desenho, x: x - Math.floor(largura(desenho) / 2), y });
30
+
31
+ // Na parede, uma janela sobre cada coluna de mesas e o relógio no meio; no chão, encostadas na
32
+ // parede e fora do caminho de quem anda, duas plantas.
33
+ const DECORACAO = Object.freeze([
34
+ ...Array.from({ length: COLUNAS }, (_, coluna) => centrado(JANELA, meio(coluna), 3)),
35
+ centrado(RELOGIO, LARGURA / 2, 3),
36
+ centrado(PLANTA, LARGURA_COLUNA, 6),
37
+ centrado(PLANTA, LARGURA - LARGURA_COLUNA, 6),
38
+ ]);
39
+
40
+ /** Preenche uma matriz de pixels com o canto superior esquerdo em (x, y). */
41
+ function pintar(ctx, { paleta, linhas }, x, y) {
42
+ for (const [dx, dy, l, a, letra] of retangulos(linhas)) {
43
+ ctx.fillStyle = paleta[letra];
44
+ ctx.fillRect(x + dx, y + dy, l, a);
45
+ }
46
+ }
47
+
48
+ /** O piso: ladrilhos claros e escuros em xadrez, da parede até o fim da tela. */
49
+ function pintarPiso(ctx) {
50
+ for (let y = TOPO, linha = 0; y < ALTURA; y += altura(LADRILHO), linha++) {
51
+ for (let x = 0, coluna = 0; x < LARGURA; x += largura(LADRILHO), coluna++) {
52
+ pintar(ctx, (linha + coluna) % 2 ? LADRILHO_ESCURO : LADRILHO, x, y);
53
+ }
54
+ }
55
+ }
56
+
57
+ function pintarSala(ctx) {
58
+ for (let x = 0; x < LARGURA; x += largura(PAREDE)) pintar(ctx, PAREDE, x, 0);
59
+ pintarPiso(ctx);
60
+ for (const { desenho, x, y } of DECORACAO) pintar(ctx, desenho, x, y);
61
+ }
62
+
63
+ /** A mesa de um agente, com o monitor na cor e na fase que o quadro traz. */
64
+ function pintarMesa(ctx, { mesa, monitor, monitorFase }) {
65
+ const [x, y] = [mesa.retangulo.x + MONITOR_EM.x, mesa.retangulo.y + MONITOR_EM.y];
66
+ pintar(ctx, MESA, mesa.retangulo.x, mesa.retangulo.y - MESA_ACIMA);
67
+ pintar(ctx, MONITOR, x, y);
68
+ const tela = TELAS[monitor]?.(monitorFase);
69
+ if (tela) pintar(ctx, tela, x + TELA_EM.x, y + TELA_EM.y);
70
+ }
71
+
72
+ /** O boneco na pose do quadro, com a sombra no chão (se está em pé) e o sinal sobre a cabeça. */
73
+ function pintarBoneco(ctx, boneco) {
74
+ const { x, y, acao, pose, papel, espelhado } = boneco;
75
+ const linhas = linhasDoBoneco(pose, { papel, espelhado, festa: acao === 'comemorando' && pose === 'mao' });
76
+ const sinal = SINAIS[acao];
77
+ if (!boneco.sentado) pintar(ctx, SOMBRA, x + Math.floor((BONECO - largura(SOMBRA)) / 2), y + BONECO);
78
+ ctx.globalAlpha = acao === 'skipped' ? ALFA_DO_PULADO : 1;
79
+ pintar(ctx, { linhas, paleta: coresDoBoneco(boneco.aparencia) }, x, y);
80
+ ctx.globalAlpha = 1;
81
+ if (sinal) pintar(ctx, sinal, x + Math.floor((BONECO - largura(sinal)) / 2), y - altura(sinal) - 1);
82
+ }
83
+
84
+ /**
85
+ * Pinta um quadro inteiro, em pixel lógico: a sala e, do fundo para a frente, mesas e bonecos.
86
+ * Quem está mais abaixo na tela fica na frente: o boneco sentado é pintado antes da própria
87
+ * mesa (o tampo cobre as pernas dele) e quem entrega, parado diante de outra mesa, depois dela.
88
+ * @param {object} ctx o contexto 2D (ou qualquer objeto com `fillStyle`, `globalAlpha` e `fillRect`)
89
+ * @param {object[]} bonecos o que `quadro` devolveu; só há mesa para quem está nesta lista
90
+ */
91
+ export function pintarCena(ctx, bonecos) {
92
+ pintarSala(ctx);
93
+ const camadas = bonecos.flatMap((boneco) => [
94
+ { base: boneco.mesa.retangulo.y + boneco.mesa.retangulo.altura, pintar: pintarMesa, boneco },
95
+ { base: boneco.y + BONECO, pintar: pintarBoneco, boneco },
96
+ ]);
97
+ camadas.sort((a, b) => a.base - b.base);
98
+ for (const camada of camadas) camada.pintar(ctx, camada.boneco);
99
+ }
100
+
101
+ /** O que muda o desenho de um boneco: se nada disto mudou, o quadro é o mesmo. */
102
+ const resumo = (b) => [b.id, b.indice, b.acao, b.pose, b.papel, b.espelhado, b.x, b.y, b.monitor, b.monitorFase].join(':');
103
+
104
+ /**
105
+ * O laço de animação. A cada quadro do navegador pede os bonecos e, se algo mudou desde a última
106
+ * pintura (ou o canvas mudou de tamanho), pinta na escala inteira do canvas, sem suavização.
107
+ * Um quadro que falha não para o laço.
108
+ * @param {object} janela de quem vem o `requestAnimationFrame`
109
+ * @param {object} tela o canvas, já com 320·N × 180·N pixels
110
+ * @param {() => object[] | null} obterQuadro os bonecos deste instante; `null`: ainda não há o que pintar
111
+ * @param {() => void} aoPintar chamada depois de cada pintura
112
+ */
113
+ export function animar(janela, tela, obterQuadro, aoPintar) {
114
+ const ctx = tela.getContext('2d');
115
+ let pintado = '';
116
+ function passo() {
117
+ try {
118
+ const bonecos = obterQuadro();
119
+ const marca = bonecos ? `${tela.width}|${bonecos.map(resumo).join(';')}` : pintado;
120
+ if (marca === pintado) return;
121
+ const n = tela.width / LARGURA;
122
+ ctx.setTransform(n, 0, 0, n, 0, 0);
123
+ ctx.imageSmoothingEnabled = false;
124
+ pintarCena(ctx, bonecos);
125
+ pintado = marca;
126
+ aoPintar();
127
+ } finally {
128
+ janela.requestAnimationFrame(passo);
129
+ }
130
+ }
131
+ janela.requestAnimationFrame(passo);
132
+ }