@aksp/opencrew 1.6.1 → 1.6.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.
- package/CHANGELOG.md +66 -0
- package/README.md +20 -8
- package/package.json +2 -2
- package/src/cli.js +15 -38
- package/src/commands/init.js +41 -44
- package/src/commands/update.js +78 -75
- package/src/lib/blocos.js +148 -0
- package/src/lib/deteccao.js +69 -0
- package/src/lib/fsx.js +1 -55
- package/src/lib/ides.js +4 -0
- package/src/lib/legado.js +142 -0
- package/src/lib/manifest.js +67 -26
- package/src/lib/mcp.js +131 -0
- package/src/lib/migrations.js +77 -74
- package/src/lib/node-version.js +43 -0
- package/src/lib/prompts.js +25 -2
- package/src/lib/resumo.js +125 -0
- package/templates/.mcp.json +1 -1
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
- package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
- package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
- package/templates/_opencrew/core/runner.pipeline.md +32 -12
- package/templates/_opencrew/core/scripts/comum.mjs +49 -4
- package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +32 -1
- package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
- package/templates/_opencrew/core/skills.engine.md +7 -3
- package/templates/gitignore +1 -0
- package/templates/skills/blotato/SKILL.md +39 -10
- package/templates/skills/image-ai-generator/SKILL.md +18 -5
- package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
- package/templates/skills/instagram-publisher/SKILL.md +4 -0
- package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
- package/templates/skills/resend/SKILL.md +52 -13
|
@@ -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.
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
|
@@ -50,7 +50,7 @@ Transform markdown content into a PDF file using Playwright (already available i
|
|
|
50
50
|
4. Write the HTML to a temporary file: `crews/{crew-name}/output/{run_id}/export/temp.html`
|
|
51
51
|
5. Use Playwright to render the HTML as PDF:
|
|
52
52
|
```bash
|
|
53
|
-
npx playwright open --viewport=1240,1754 crews/{crew-name}/output/{run_id}/export/temp.html
|
|
53
|
+
npx playwright open --viewport=1240,1754 "crews/{crew-name}/output/{run_id}/export/temp.html"
|
|
54
54
|
```
|
|
55
55
|
Then use the print-to-PDF functionality.
|
|
56
56
|
6. Save the PDF to the step's `outputFile` path
|
|
@@ -111,12 +111,12 @@ Browser sessions are stored as JSON files in `_opencrew/_browser_profile/`:
|
|
|
111
111
|
|
|
112
112
|
**Loading a session (Playwright CLI):**
|
|
113
113
|
```bash
|
|
114
|
-
npx playwright open --load-storage=_opencrew/_browser_profile/{platform}.json {url}
|
|
114
|
+
npx playwright open --load-storage=_opencrew/_browser_profile/{platform}.json "{url}"
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
**Saving a session (Playwright CLI):**
|
|
118
118
|
```bash
|
|
119
|
-
npx playwright open --save-storage=_opencrew/_browser_profile/{platform}.json {url}
|
|
119
|
+
npx playwright open --save-storage=_opencrew/_browser_profile/{platform}.json "{url}"
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
When using MCP browser tools or other automation APIs, use these JSON files as the source of truth for session state — load the stored cookies/localStorage at the start of each investigation and save after login when the user consents.
|
|
@@ -210,7 +210,7 @@ On the first investigation for a given platform, Sherlock may encounter a login
|
|
|
210
210
|
|
|
211
211
|
4. **Step 1 — Open browser for login (NO session saving):**
|
|
212
212
|
```bash
|
|
213
|
-
npx playwright open {platform-url}
|
|
213
|
+
npx playwright open "{platform-url}"
|
|
214
214
|
```
|
|
215
215
|
Use a **5-minute timeout** on this command. The user needs time to complete login + any verification (email, SMS, 2FA).
|
|
216
216
|
|
|
@@ -220,7 +220,7 @@ On the first investigation for a given platform, Sherlock may encounter a login
|
|
|
220
220
|
Ask: "Want me to save this session for next time?"
|
|
221
221
|
If yes:
|
|
222
222
|
```bash
|
|
223
|
-
npx playwright open --save-storage=_opencrew/_browser_profile/{platform}.json {platform-url}
|
|
223
|
+
npx playwright open --save-storage=_opencrew/_browser_profile/{platform}.json "{platform-url}"
|
|
224
224
|
```
|
|
225
225
|
This command completes quickly since the browser already has the authenticated cookies.
|
|
226
226
|
|
|
@@ -231,7 +231,7 @@ On the first investigation for a given platform, Sherlock may encounter a login
|
|
|
231
231
|
|
|
232
232
|
After the first login, load the session file at the start of each investigation:
|
|
233
233
|
```bash
|
|
234
|
-
npx playwright open --load-storage=_opencrew/_browser_profile/{platform}.json {url}
|
|
234
|
+
npx playwright open --load-storage=_opencrew/_browser_profile/{platform}.json "{url}"
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
Still check for login walls on each run (platforms may expire sessions) and re-prompt the user if needed.
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
|
|
6
6
|
You are the Pipeline Runner. Your job is to execute a crew's pipeline step by step.
|
|
7
7
|
|
|
8
|
+
## Safe names in commands (nome seguro)
|
|
9
|
+
|
|
10
|
+
Applies to EVERY command below and in any prompt or skill. A path of the crew or of the user's
|
|
11
|
+
project goes into a command only between double quotes and only if it is made of letters (accents
|
|
12
|
+
included), digits, space and `. _ - / \ : ( )`. With any other character (`$`, backtick, quote,
|
|
13
|
+
`%`, `!`, `,`, `;`, `&`, `|`, `<`, `>`, line break) do NOT build the command:
|
|
14
|
+
- **Crew folder** → stop: "⚠️ A pasta da crew (`crews/{name}`) tem um caractere que não posso usar
|
|
15
|
+
em comandos ({caractere}). Renomeie a pasta e rode de novo."
|
|
16
|
+
- **Output file** (`inputFile`, `outputFile`, any file passed to a script) → ask and wait:
|
|
17
|
+
```
|
|
18
|
+
⚠️ O nome `{caminho}` tem um caractere que não posso usar em comandos ({caractere}). Use só letras, números, espaço, ponto, hífen, sublinhado e parênteses.
|
|
19
|
+
1. Parar para você renomear (ajuste também o `outputFile` do passo)
|
|
20
|
+
2. Seguir sem conferir este arquivo
|
|
21
|
+
```
|
|
22
|
+
On 2, run no command with that file (no validation gate, not sent to the checker) and list it at
|
|
23
|
+
the final approval: `{arquivo} — não verificado: nome com caractere que não vai em comando`.
|
|
24
|
+
|
|
8
25
|
## Initialization
|
|
9
26
|
|
|
10
27
|
Before starting execution:
|
|
@@ -49,7 +66,7 @@ Before starting execution:
|
|
|
49
66
|
|
|
50
67
|
1b. **Memory format migration** — After loading `memories.md`, check whether it uses the new format by scanning for the `## Estilo de Escrita` section header:
|
|
51
68
|
```bash
|
|
52
|
-
[ -f crews/{name}/_memory/memories.md ] && grep -q "## Estilo de Escrita" crews/{name}/_memory/memories.md && echo "NEW_FORMAT" || echo "OLD_FORMAT"
|
|
69
|
+
[ -f "crews/{name}/_memory/memories.md" ] && grep -q "## Estilo de Escrita" "crews/{name}/_memory/memories.md" && echo "NEW_FORMAT" || echo "OLD_FORMAT"
|
|
53
70
|
```
|
|
54
71
|
- If `NEW_FORMAT` → proceed normally.
|
|
55
72
|
- If `OLD_FORMAT` (or file is empty / does not exist) → migrate before proceeding:
|
|
@@ -74,7 +91,7 @@ Before starting execution:
|
|
|
74
91
|
(Use the crew's display name for `{crew-name}`, and the crew code for `{name}` in file paths — they refer to the same crew.)
|
|
75
92
|
b. Check if `crews/{name}/_memory/runs.md` exists:
|
|
76
93
|
```bash
|
|
77
|
-
test -f crews/{name}/_memory/runs.md && echo "EXISTS" || echo "MISSING"
|
|
94
|
+
test -f "crews/{name}/_memory/runs.md" && echo "EXISTS" || echo "MISSING"
|
|
78
95
|
```
|
|
79
96
|
If `MISSING`, create it with:
|
|
80
97
|
```markdown
|
|
@@ -102,9 +119,10 @@ Before starting execution:
|
|
|
102
119
|
On 1, run the same command with `--corrigir`, show the new result and re-read `crew.yaml` and
|
|
103
120
|
any agent file already loaded (it may have changed them); 1d then loads the sources from the
|
|
104
121
|
corrected paths. If the new result still ends in `FONTES:PENDENTE`, ask again with options 2 and
|
|
105
|
-
3 only.
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
3 only. Alerts — not portable (absolute paths) or "não conferido" (a network path or a site
|
|
123
|
+
address: the script never accesses the network) — are mentioned once, without stopping. If the
|
|
124
|
+
script did not run (no Node, an error, or no `FONTES:` status line), tell the user "⚠️ A
|
|
125
|
+
conferência de fontes não rodou: {motivo}" and continue; the final approval repeats the warning.
|
|
108
126
|
|
|
109
127
|
1d. **Project sources (`fontes:`)** — if `crew.yaml` has a `fontes:` list (files or folders of
|
|
110
128
|
the user's project, paths relative to the project root), read them now: a file in full up to
|
|
@@ -224,7 +242,7 @@ Before starting execution:
|
|
|
224
242
|
- Format: `YYYY-MM-DD-HHmmss` using the current timestamp (e.g. `2026-03-03-143022`)
|
|
225
243
|
- Check if `crews/{name}/output/{run_id}/` already exists
|
|
226
244
|
- If it does (sub-second collision), append `-2`, `-3`, etc. until the folder does not exist
|
|
227
|
-
- Create the folder using Bash: `mkdir -p crews/{name}/output/{run_id}`
|
|
245
|
+
- Create the folder using Bash: `mkdir -p "crews/{name}/output/{run_id}"`
|
|
228
246
|
- Store `run_id` in working memory for this run — it will be used for ALL output paths
|
|
229
247
|
6. **Initialize state.json** (only if `dashboard_enabled` — see step 1a; otherwise skip this entire step, including all sub-steps below):
|
|
230
248
|
- **IMPORTANT**: When enabled, write to `crews/{name}/state.json` before every step and after every handoff, as described throughout this document. When `dashboard_enabled` is false, never create, write, or delete this file.
|
|
@@ -307,13 +325,14 @@ Before executing any step that references an agent:
|
|
|
307
325
|
```
|
|
308
326
|
If the step has no `format:` field, skip this step entirely (backward compatible).
|
|
309
327
|
6. **Inject skill context (Two-Tier)**:
|
|
310
|
-
a. Build a Tier 1 skill index from each declared skill's frontmatter `name` and `
|
|
311
|
-
b. Append the index after format injection:
|
|
328
|
+
a. Build a Tier 1 skill index from each declared skill's frontmatter `name`, `description` and `side_effects` (~30 tokens per skill)
|
|
329
|
+
b. Append the index after format injection (the second form is for every skill with `side_effects: irreversible`):
|
|
312
330
|
```
|
|
313
331
|
--- AVAILABLE SKILLS ---
|
|
314
332
|
- {skill-id}: {description} (type: {type})
|
|
333
|
+
- {skill-id}: {description} (type: {type}) — irreversível: carregue as instruções desta skill e peça a confirmação antes de usar
|
|
315
334
|
```
|
|
316
|
-
c. If the step's frontmatter contains `skills_needed: [...]`, load Tier 2 (full SKILL.md body) for those skills immediately
|
|
335
|
+
c. If the step's frontmatter contains `skills_needed: [...]`, load Tier 2 (full SKILL.md body) for those skills immediately; for a skill with `side_effects: irreversible`, always load Tier 2 before its first use
|
|
317
336
|
d. Otherwise, Tier 2 is loaded on-demand when the agent invokes a skill during execution
|
|
318
337
|
e. See `_opencrew/core/skills.engine.md` Operation 6 for full details
|
|
319
338
|
|
|
@@ -464,7 +483,7 @@ Apply to every path that was transformed in Step 1:
|
|
|
464
483
|
|
|
465
484
|
2. Detect existing versions for this group using Bash:
|
|
466
485
|
```bash
|
|
467
|
-
ls -1 crews/{name}/output/{run_id}/{relative-group}/ 2>/dev/null | grep -E '^v[0-9]+$' | sort -V | tail -1
|
|
486
|
+
ls -1 "crews/{name}/output/{run_id}/{relative-group}/" 2>/dev/null | grep -E '^v[0-9]+$' | sort -V | tail -1
|
|
468
487
|
```
|
|
469
488
|
- If the command returns a version (e.g. `v2`) → use `v3`
|
|
470
489
|
(Always increment the highest version found, even if lower versions have gaps — e.g. if `v1` and `v3` exist, use `v4`)
|
|
@@ -732,7 +751,8 @@ When a step has `on_reject: {step-id}` (a review step):
|
|
|
732
751
|
of alerts and the {Z} items not measured or not verified (the `Não medido` and `Não verificado`
|
|
733
752
|
lines under each file, not the "não é texto" line of **Notas**), one per line as
|
|
734
753
|
`{arquivo} — {motivo}`, then the lines under `**Notas:**` in that report, as they are written,
|
|
735
|
-
and repeat every "não rodou" warning of this run (checker and source check)
|
|
754
|
+
and repeat every "não rodou" warning of this run (checker and source check) and the line of
|
|
755
|
+
every file left unchecked by the safe-name rule. If the approved
|
|
736
756
|
text still contains `[PREENCHER: …]`, ask the user for each missing piece of real information
|
|
737
757
|
and write it into the text before approving.
|
|
738
758
|
|
|
@@ -801,7 +821,7 @@ After writing the final "completed" state to `crews/{name}/state.json`:
|
|
|
801
821
|
1. Add the `completedAt` field (or `failedAt` if status is `failed`) with the current ISO timestamp
|
|
802
822
|
2. Copy `state.json` to the run output folder for permanent history:
|
|
803
823
|
```bash
|
|
804
|
-
cp crews/{name}/state.json crews/{name}/output/{run_id}/state.json
|
|
824
|
+
cp "crews/{name}/state.json" "crews/{name}/output/{run_id}/state.json"
|
|
805
825
|
```
|
|
806
826
|
3. Leave the working copy of `crews/{name}/state.json` in place — do not delete it and
|
|
807
827
|
do not add an artificial delay. A dashboard watching the file already sees the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Validações e mensagens de erro de uso comuns aos scripts do runtime (verificar,
|
|
2
2
|
// conferir-fontes…). Node puro, sem dependências.
|
|
3
|
-
//
|
|
3
|
+
// Specs: specs/fase-r1-reparos-1-6-1.md, regra 13, e specs/fase-r2-update-e-envio-seguros.md,
|
|
4
|
+
// regra 23 (repositório do OpenCrew).
|
|
4
5
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -12,12 +13,56 @@ export const MSG = {
|
|
|
12
13
|
crewNaoEncontrada: (crew) => `Crew não encontrada: ${crew}`,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
export
|
|
17
|
-
|
|
16
|
+
/** Caminho de rede: começa por duas barras (`\\` ou `//`), em qualquer sistema. Nunca vai ao disco. */
|
|
17
|
+
export const ehDeRede = (caminho) => /^[\\/]{2}/.test(caminho);
|
|
18
|
+
|
|
19
|
+
/** Pelo texto: `alvo` é a `pasta` ou fica dentro dela? */
|
|
20
|
+
function contem(pasta, alvo) {
|
|
21
|
+
const rel = path.relative(pasta, alvo);
|
|
18
22
|
return !(rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel));
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Lugar real de um caminho: link, junção e nome curto resolvidos. O trecho que ainda não existe é
|
|
27
|
+
* juntado, como foi escrito, à pasta mais funda que existe. Caminho de rede não é consultado: vale
|
|
28
|
+
* o texto.
|
|
29
|
+
*/
|
|
30
|
+
export function lugarReal(caminho) {
|
|
31
|
+
const abs = path.resolve(caminho);
|
|
32
|
+
if (ehDeRede(caminho) || ehDeRede(abs)) return abs;
|
|
33
|
+
try {
|
|
34
|
+
return realpathSync.native(abs);
|
|
35
|
+
} catch {
|
|
36
|
+
const pai = path.dirname(abs);
|
|
37
|
+
return pai === abs ? abs : path.join(lugarReal(pai), path.basename(abs));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pelo lugar real: `caminho` é a `pasta` ou fica dentro dela? */
|
|
42
|
+
export const realDentroDe = (pasta, caminho) => contem(lugarReal(pasta), lugarReal(caminho));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* O caminho (relativo à raiz ou absoluto) fica dentro do projeto? Sim quando o texto ou o lugar
|
|
46
|
+
* real diz "dentro"; não, só quando os dois dizem "fora" (regra 23). Raiz e caminho são resolvidos
|
|
47
|
+
* pela mesma função, e o lugar real só é consultado quando o texto diz "fora". Caminho de rede só
|
|
48
|
+
* vale pelo texto, com o projeto também na rede: o disco não é tocado, em nenhum sistema.
|
|
49
|
+
*/
|
|
50
|
+
export function dentroDoProjeto(raiz, caminho) {
|
|
51
|
+
const alvo = path.resolve(raiz, caminho);
|
|
52
|
+
if (ehDeRede(caminho)) return ehDeRede(raiz) && contem(raiz, alvo);
|
|
53
|
+
return contem(raiz, alvo) || realDentroDe(raiz, alvo);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Caminho de dentro do projeto, relativo à raiz e com `/`: pelo texto quando o texto já fica
|
|
58
|
+
* dentro; senão, pelo lugar real (link, junção ou nome curto que leva ao projeto).
|
|
59
|
+
*/
|
|
60
|
+
export function relativoAoProjeto(raiz, caminho) {
|
|
61
|
+
const alvo = path.resolve(raiz, caminho);
|
|
62
|
+
const [de, para] = contem(raiz, alvo) ? [raiz, alvo] : [lugarReal(raiz), lugarReal(alvo)];
|
|
63
|
+
return path.relative(de, para).split(path.sep).join('/');
|
|
64
|
+
}
|
|
65
|
+
|
|
21
66
|
/**
|
|
22
67
|
* O script foi chamado direto (`node …/script.mjs`)? Compara os caminhos reais: com o projeto
|
|
23
68
|
* aberto por uma junção ou um link de pasta, o Node resolve o link em `import.meta.url` e não em
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Busca da conferência de fontes: onde está cada caminho citado (crew → raiz do projeto →
|
|
2
2
|
// absoluto → ao lado do agente ou da task que cita) e, quando ele sumiu, o que existe no projeto
|
|
3
|
-
// com o mesmo nome. Só testa existência e lista nomes; nunca lê conteúdo.
|
|
4
|
-
//
|
|
3
|
+
// com o mesmo nome. Só testa existência e lista nomes; nunca lê conteúdo nem acessa a rede.
|
|
4
|
+
// Specs: specs/fase-r1-reparos-1-6-1.md, regra 17, e specs/fase-r2-update-e-envio-seguros.md,
|
|
5
|
+
// regra 25 (repositório do OpenCrew).
|
|
5
6
|
import { readdir } from 'node:fs/promises';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
+
import { existsSync, statSync } from 'node:fs';
|
|
7
8
|
import path from 'node:path';
|
|
9
|
+
import { ehDeRede } from '../comum.mjs';
|
|
8
10
|
|
|
9
11
|
export const LIMITE_DA_BUSCA = 20000;
|
|
10
12
|
const IGNORAR = new Set(['node_modules', 'output', '_opencrew', '_build']);
|
|
@@ -13,6 +15,7 @@ export const barra = (p) => p.split(path.sep).join('/');
|
|
|
13
15
|
export const ehAbsoluto = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('/');
|
|
14
16
|
export const temBarraFinal = (ref) => /[\\/]$/.test(ref);
|
|
15
17
|
const semBarraFinal = (ref) => ref.replace(/[\\/]+$/, '');
|
|
18
|
+
const ehPasta = (p) => Boolean(p) && Boolean(statSync(p, { throwIfNoEntry: false })?.isDirectory());
|
|
16
19
|
|
|
17
20
|
const SUFIXO_DE_AGENTE = '.agent.md';
|
|
18
21
|
|
|
@@ -33,8 +36,10 @@ function pastasDeQuemCita(raiz, crew, citadoEm) {
|
|
|
33
36
|
/**
|
|
34
37
|
* Caminho real do que foi citado, ou null quando não existe. Ordem: pasta da crew → raiz do
|
|
35
38
|
* projeto → absoluto → pastas ao lado dos arquivos de agente ou de task que citam (`citadoEm`).
|
|
39
|
+
* Citação de duas barras (caminho de rede) nunca chega ao disco: sai como "não existe".
|
|
36
40
|
*/
|
|
37
41
|
export function resolver(raiz, crew, ref, citadoEm = []) {
|
|
42
|
+
if (ehDeRede(ref)) return null;
|
|
38
43
|
if (ehAbsoluto(ref)) return existsSync(ref) ? path.resolve(ref) : null;
|
|
39
44
|
for (const base of [path.resolve(raiz, crew), raiz, ...pastasDeQuemCita(raiz, crew, citadoEm)]) {
|
|
40
45
|
const p = path.resolve(base, ref);
|
|
@@ -43,6 +48,40 @@ export function resolver(raiz, crew, ref, citadoEm = []) {
|
|
|
43
48
|
return null;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* O primeiro segmento de `exemplo.com/blog/`, quando tem cara de domínio: um ponto que não é o
|
|
53
|
+
* início e, depois do último ponto, 2 a 24 letras. Nome sem barra (`briefing.md`) é arquivo: null.
|
|
54
|
+
*/
|
|
55
|
+
function dominioNoInicio(ref) {
|
|
56
|
+
const corte = ref.search(/[\\/]/);
|
|
57
|
+
const primeiro = ref.slice(0, Math.max(corte, 0));
|
|
58
|
+
const ponto = primeiro.lastIndexOf('.');
|
|
59
|
+
return ponto > 0 && /^[A-Za-z]{2,24}$/.test(primeiro.slice(ponto + 1)) ? primeiro : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Citação que a conferência não testa (regra 25 da fase R2): caminho de rede (começa por duas
|
|
64
|
+
* barras) ou endereço de site — tem `://` (menos `http(s)://`, que fica como sempre foi), começa
|
|
65
|
+
* por `mailto:` ou `www.`. Caminho de disco (letra de unidade ou uma barra no início) nunca é
|
|
66
|
+
* endereço. Não consulta o disco nem a rede.
|
|
67
|
+
*/
|
|
68
|
+
export function ehRedeOuSite(ref) {
|
|
69
|
+
if (ehDeRede(ref)) return true;
|
|
70
|
+
if (ehAbsoluto(ref) || /^https?:\/\//i.test(ref)) return false;
|
|
71
|
+
return ref.includes('://') || /^(?:mailto:|www\.)/i.test(ref);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Citação que só parece endereço de site pela forma: o primeiro segmento tem cara de domínio e
|
|
76
|
+
* não há pasta com esse nome no projeto. Quem chama confere antes se o projeto tem arquivo com o
|
|
77
|
+
* mesmo nome: pasta com ponto que foi movida continua pendência, com sugestão.
|
|
78
|
+
*/
|
|
79
|
+
export function pareceSite({ raiz, crew }, { ref, citadoEm }) {
|
|
80
|
+
if (ehAbsoluto(ref) || /^https?:\/\//i.test(ref)) return false;
|
|
81
|
+
const dominio = dominioNoInicio(ref);
|
|
82
|
+
return dominio != null && !ehPasta(resolver(raiz, crew, dominio, citadoEm));
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
/**
|
|
47
86
|
* Índice nome → caminhos relativos à raiz (pasta termina em `/`), sem saídas, dependências e
|
|
48
87
|
* pastas ocultas. Para ao passar de `limite` itens: aí `parcial` é true.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Relatório da conferência de fontes e as mensagens ao usuário (PT-BR).
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
2
|
+
// Specs: specs/fase-r1-reparos-1-6-1.md, regra 17 e seção 6, e
|
|
3
|
+
// specs/fase-r2-update-e-envio-seguros.md, regras 24 a 26 e seção 6 (repositório do OpenCrew).
|
|
4
|
+
import { relativoAoProjeto } from '../comum.mjs';
|
|
5
5
|
|
|
6
6
|
const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
|
|
7
7
|
|
|
@@ -10,6 +10,9 @@ export const MSG = {
|
|
|
10
10
|
corrigidos: (n) => `${plural(n, 'caminho corrigido', 'caminhos corrigidos')} (cópia .bak ao lado de cada arquivo alterado).\n`,
|
|
11
11
|
semCorrecaoAutomatica: (n) => `Não há correção automática para ${n} pendência(s): escolha um candidato ou corrija o caminho na crew.`,
|
|
12
12
|
buscaParcial: (limite) => `Procurei só nos primeiros ${limite} itens do projeto; pode existir um arquivo com esse nome que eu não vi.`,
|
|
13
|
+
linkParaFora: (arquivo) => `Não corrigi \`${arquivo}\`: é um link que aponta para fora da crew. O caminho citado nele continua como estava.`,
|
|
14
|
+
crewLigadaParaFora: (crew) => `Não corrigi nada: a pasta \`${crew}\` é um link que aponta para fora do projeto.`,
|
|
15
|
+
naoConferi: (motivo) => `Não consegui conferir: ${motivo}`,
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
const MAX_NOMES = 20;
|
|
@@ -37,16 +40,25 @@ function linhaDoAlerta(i, onde) {
|
|
|
37
40
|
return `- ⚠️ \`${i.ref}\` é um caminho absoluto (não é portátil — quebra em outro computador).${sugestao} (${onde})`;
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/** Caminho de rede ou endereço de site: a citação aparece, e o relatório diz que não foi testada. */
|
|
44
|
+
function linhaDoNaoConferido(i, onde) {
|
|
45
|
+
return `- ⚠️ \`${i.ref}\` é um caminho de rede ou um endereço de site: não conferi se existe (a conferência não acessa a rede). (${onde})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Os dois estados de alerta (não mudam o status); qualquer outro estado apontado é pendência.
|
|
49
|
+
const LINHA_DO_ALERTA = { 'nao-portatil': linhaDoAlerta, 'nao-conferido': linhaDoNaoConferido };
|
|
50
|
+
|
|
40
51
|
export function formatar(r) {
|
|
41
52
|
const aviso = r.buscaParcial ? MSG.buscaParcial(r.limite) : '';
|
|
42
53
|
const contar = (estado) => r.refs.filter((i) => i.estado === estado).length;
|
|
43
54
|
const apontados = r.refs.filter((x) => x.estado !== 'ok');
|
|
44
55
|
const linhas = [`## Conferência de fontes — ${r.crew}`, ''];
|
|
45
56
|
for (const i of apontados) {
|
|
46
|
-
const onde = `citado em ${i.citadoEm.map((a) =>
|
|
47
|
-
linhas.push(i.estado
|
|
57
|
+
const onde = `citado em ${i.citadoEm.map((a) => relativoAoProjeto(r.raiz, a)).join(', ')}`;
|
|
58
|
+
linhas.push((LINHA_DO_ALERTA[i.estado] ?? linhaDaPendencia)(i, onde, aviso));
|
|
48
59
|
}
|
|
49
60
|
if (apontados.length) linhas.push(''); // sem pendência nem alerta, uma linha em branco só
|
|
50
|
-
|
|
61
|
+
const alertas = contar('nao-portatil') + contar('nao-conferido');
|
|
62
|
+
linhas.push(`**Resumo: ${r.refs.length} fontes — ${contar('ok')} ok, ${contar('faltando')} pendentes, ${alertas} alertas**`, '');
|
|
51
63
|
return linhas.join('\n');
|
|
52
64
|
}
|