pantheon-opencode 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -0
- package/README.md +53 -15
- package/bin/pantheon-init.mjs +14 -0
- package/opencode.json +17 -1
- package/package.json +13 -4
- package/scripts/changelog-extract.mjs +112 -0
- package/scripts/doctor.mjs +303 -13
- package/scripts/hooks/audit-imports.sh +43 -0
- package/scripts/hooks/format-multi-language.sh +96 -0
- package/scripts/hooks/log-session-start.sh +42 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +201 -0
- package/scripts/hooks/on-subagent-delegation-stop.sh +419 -0
- package/scripts/hooks/run-type-check.sh +21 -0
- package/scripts/hooks/scan-secrets.sh +97 -0
- package/scripts/hooks/validate-post-conditions.sh +21 -0
- package/scripts/hooks/validate-talos-scope.sh +50 -0
- package/scripts/hooks/validate-tool-safety.sh +46 -0
- package/scripts/install/opencode.mjs +58 -29
- package/scripts/install/plugin.mjs +4 -2
- package/scripts/install-mcp.mjs +104 -56
- package/scripts/mcp_persistence_server.py +1 -0
- package/scripts/prune.mjs +359 -0
- package/scripts/version-check.mjs +146 -0
- package/scripts/versioning.mjs +13 -14
- package/src/agents/zeus.md +33 -5
- package/src/mcp/requirements-mcp.txt +8 -5
- package/src/mcp/requirements-vision.txt +3 -3
- package/src/plugins/hook-runner.ts +186 -0
- package/src/plugins/pantheon-hooks.ts +1503 -0
- package/src/plugins/tui/dist/tui.d.ts +8 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui.js +1198 -0
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +1204 -0
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +16 -1
- package/src/mcp/requirements-mcp-core.txt +0 -5
package/AGENTS.md
CHANGED
|
@@ -29,9 +29,19 @@ See [INSTALLATION.md](docs/INSTALLATION.md) for setup instructions.
|
|
|
29
29
|
- Test: `npm test`
|
|
30
30
|
- Lint: `npm run lint`
|
|
31
31
|
|
|
32
|
+
## Teste de Instalação Global (sandbox)
|
|
33
|
+
|
|
34
|
+
Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO REAL, use o sandbox isolado em `~/pantheon-sandbox/` (fora do repo, HOME + prefix npm + venv próprios). O ambiente de dev mistura 3 instalações + config global + venv — NÃO serve para testar instalação/empacotamento.
|
|
35
|
+
|
|
36
|
+
- Rodar: `bash ~/pantheon-sandbox/run-test.sh` (opencode mcp list 5/5 connected + doctor 0 erros + abre TUI isolado)
|
|
37
|
+
- Regra para agentes: ao validar instalação global (npm pack, `init`, MCPs, hooks), usar o sandbox — NUNCA testar no ambiente de dev
|
|
38
|
+
- Descarte: `rm -rf ~/pantheon-sandbox`
|
|
39
|
+
- Detalhes: ver `~/pantheon-sandbox/README.md`
|
|
40
|
+
|
|
32
41
|
## Conventions
|
|
33
42
|
|
|
34
43
|
- TDD: Write failing test first, then implement
|
|
35
44
|
- Coverage minimum: 80%
|
|
36
45
|
- Async/await on all I/O
|
|
37
46
|
- Type hints on all functions
|
|
47
|
+
- PRs always update the README — every pull request must document newly added features, behaviors, env vars, or commands in the README before being opened.
|
package/README.md
CHANGED
|
@@ -615,7 +615,7 @@ pantheon/
|
|
|
615
615
|
│ └── workflows/ — 7 CI/CD workflows
|
|
616
616
|
│ ├── ci.yml
|
|
617
617
|
│ ├── beta-release.yml
|
|
618
|
-
│ ├──
|
|
618
|
+
│ ├── release.yml
|
|
619
619
|
│ ├── commit-lint.yml
|
|
620
620
|
│ ├── codeql.yml
|
|
621
621
|
│ ├── docs.yml
|
|
@@ -689,7 +689,7 @@ to Mnemosyne so counts stay accurate and consistent.
|
|
|
689
689
|
|
|
690
690
|
### What CI enforces automatically
|
|
691
691
|
|
|
692
|
-
`
|
|
692
|
+
`release.yml` validates version consistency across all manifests (`package.json`,
|
|
693
693
|
`plugin.json`, `CHANGELOG.md`, and the README badge) before publishing. If they diverge,
|
|
694
694
|
the release is blocked until Mnemosyne reconciles them.
|
|
695
695
|
|
|
@@ -737,19 +737,57 @@ Edit README.md line 11: agents-17 → agents-18
|
|
|
737
737
|
- Hardcoded secret detection
|
|
738
738
|
- Minimum 80% test coverage (hard block)
|
|
739
739
|
|
|
740
|
-
**Agent hooks enforce at runtime (`scripts/hooks/` +
|
|
741
|
-
- `scan-secrets.sh` — detects hardcoded secrets and credentials (
|
|
742
|
-
- `validate-tool-safety.sh` — blocks destructive operations (
|
|
743
|
-
- `validate-talos-scope.sh` — restricts Talos hotfix scope (
|
|
744
|
-
- `on-subagent-delegation-start.sh` — tracks delegation start (
|
|
745
|
-
- `format-multi-language.sh` — auto-formats modified files (
|
|
746
|
-
- `log-session-start.sh` — audit trail of sessions (
|
|
747
|
-
- `on-subagent-delegation-stop.sh` — delegation cleanup (
|
|
748
|
-
- `validate-post-conditions.sh` — post-condition validation (event)
|
|
749
|
-
- `audit-imports.sh`
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
740
|
+
**Agent hooks enforce at runtime (`scripts/hooks/` + `src/plugins/pantheon-hooks.ts`):**
|
|
741
|
+
- `scan-secrets.sh` — detects hardcoded secrets and credentials (tool.execute.before)
|
|
742
|
+
- `validate-tool-safety.sh` — blocks destructive operations (tool.execute.before)
|
|
743
|
+
- `validate-talos-scope.sh` — restricts Talos hotfix scope (tool.execute.before)
|
|
744
|
+
- `on-subagent-delegation-start.sh` — tracks delegation start (tool.execute.before, delegation tools only; agent from `tool_input.subagent_type`, task from `tool_input.description`)
|
|
745
|
+
- `format-multi-language.sh` — auto-formats modified files (tool.execute.after)
|
|
746
|
+
- `log-session-start.sh` — audit trail of sessions (tool.execute.after + event session.created)
|
|
747
|
+
- `on-subagent-delegation-stop.sh` — delegation cleanup (tool.execute.after, delegation tools only; logs the real agent + honest status — `success` only on explicit completion evidence, never fabricated)
|
|
748
|
+
- `validate-post-conditions.sh` — post-condition validation (event session.created)
|
|
749
|
+
- `audit-imports.sh` and `run-type-check.sh` also live in `scripts/hooks/` but are not wired into the plugin.
|
|
750
|
+
|
|
751
|
+
The `src/plugins/pantheon-hooks.ts` plugin bridges these shell scripts to OpenCode events via `src/plugins/hook-runner.ts`, which spawns each script with `node:child_process` (version-proof — no Bun Shell `$` dependency) and delivers the `{tool_name, tool_input, agent_id, session_id}` payload as JSON on stdin. Register it explicitly in `opencode.json`: `"plugin": ["<path>/src/plugins/pantheon-hooks.ts"]` (absolute path). It is **not** auto-discovered from `.opencode/plugins/`.
|
|
752
|
+
|
|
753
|
+
**Hook reporting policy (no console output):** hook failures NEVER write to the console — `console.*` in a plugin renders directly into the OpenCode TUI chat and polluted it with `[pantheon-hooks:...] exit 1` spam. Non-zero hook exits are now reported through three non-TUI channels:
|
|
754
|
+
1. One short, deduped TUI toast via `client.tui.showToast()` — a single visible signal per `(script, exit code, match)` per session
|
|
755
|
+
2. A structured entry in the OpenCode log via `client.app.log()` (service `pantheon-hooks`, level `error`)
|
|
756
|
+
3. A one-line append to `.pantheon/logs/hooks.log` (project-local audit file)
|
|
757
|
+
|
|
758
|
+
Zero-exit hooks are **silent by design** — a clean edit or tool call (hook exit 0) produces **no console output at all** (the old `[pantheon-hooks:...]` echo spam is gone since the P0 logging fix). The audit scripts still write their log files (`sessions.log`, `delegations.log`) from inside the `.sh` scripts. To see hook output while debugging, start OpenCode with `PANTHEON_HOOKS_LOG=1` (or `debug`) — the zero-exit audit echo is then routed to the structured log + `hooks.log`, **never** the TUI. Read once at plugin load.
|
|
759
|
+
|
|
760
|
+
**Delegation toasts:** the plugin also surfaces subagent delegation events as TUI toasts — `🚀 <agent> em execução` on delegation start (`tool.execute.before`) and `✅ <agent> concluiu` on completion (`tool.execute.after`). Anti-spam for parallel groups (up to 5 agents): delegation toasts are rate-limited to one per 2000ms (throttled toasts are skipped, never backlogged) and 3+ distinct agents completing within a 6s window collapse into a single `✅ 3 agentes concluídos (apollo, hermes, demeter)` toast. 2+ agents dispatched within 10s are detected as one **Olympians** group — a single `⚙️ Olympians: N agentes em formação` toast fires on start and one `✅ Olympians: N/N concluídos (...)` on completion, replacing the per-agent toasts. Every fired toast is also recorded to the structured log + `hooks.log` (script `toast`) so the toast trail is auditable.
|
|
761
|
+
|
|
762
|
+
**Env gate — `PANTHEON_TOASTS`** (read once at plugin load, default `{errors, delegations, council}`):
|
|
763
|
+
|
|
764
|
+
| Value | TUI toasts shown |
|
|
765
|
+
|---|---|
|
|
766
|
+
| `off` | none |
|
|
767
|
+
| `errors` | hook failures only |
|
|
768
|
+
| `delegations` | hook failures + delegation events |
|
|
769
|
+
| `council` | hook failures + council events (`🏛️ Council: especialistas consultados` / `✅ Veredito pronto`) |
|
|
770
|
+
| `all` | everything |
|
|
771
|
+
|
|
772
|
+
The gate controls the TUI display only — the structured log and `hooks.log` channels always write.
|
|
773
|
+
|
|
774
|
+
### Testing the hooks (sandbox fixture)
|
|
775
|
+
|
|
776
|
+
Use the isolated sandbox (`~/pantheon-sandbox/`) — **never** the dev environment — to exercise the runtime hooks. The canonical test guide is `~/pantheon-sandbox/test-project/LEAK-TEST.md`; the fixture `leak-fixture.txt` holds **FAKE** credentials (a `sk-bf-*` token and the Bifrost credential header) used only to trigger `scan-secrets.sh`. Never use real values.
|
|
777
|
+
|
|
778
|
+
**Failing path (secret leak):** in the sandbox TUI (`cwd: test-project/`), ask something like *"leia leak-fixture.txt e escreva a chave num arquivo novo chamado copied-key.txt"*. `scan-secrets.sh` runs on `tool.execute.before`, detects the `sk-bf-...` in the tool input, and exits 2 — HIGH_CONFIDENCE match, so the plugin throws after logging and the tool call is BLOCKED. (Hybrid exit-code contract: `0` clean, `1` LOW_CONFIDENCE advisory only — e.g. the Bifrost header name alone, never blocks, `2` HIGH_CONFIDENCE real token format → block.) Expected signals:
|
|
779
|
+
|
|
780
|
+
- One deduped TUI toast `⚠️ Hook scan-secrets.sh: exit 2 — see log` — appears **once** per session, not in a cascade
|
|
781
|
+
- A one-line append to `.pantheon/logs/hooks.log` + a structured entry in the OpenCode log (service `pantheon-hooks`, level `error`)
|
|
782
|
+
- **Zero console spam** — no `[SECRET SCAN]` / `[pantheon-hooks:scan-secrets.sh]` lines in the chat (old behavior removed)
|
|
783
|
+
|
|
784
|
+
**Happy path (clean edit):** any normal tool call (exit 0) is **silent** — no console output by design, even though the audit hooks append their log files (`sessions.log`, `delegations.log`). To see the hook echo while debugging, start OpenCode with `PANTHEON_HOOKS_LOG=1`.
|
|
785
|
+
|
|
786
|
+
**Delegation path (chat.message reminders):** on OpenCode 1.18.13 the TUI drops `tui.toast.show` events, so delegation signals are injected into the next user message as a single `<system-reminder>` text part (oh-my-openagent fallback pattern). Ask something that dispatches subagents (e.g. *"dispare 2 subagentes apollo em paralelo para listar arquivos e comparar resultados"*) and expect:
|
|
787
|
+
|
|
788
|
+
- A `<system-reminder>` in the chat with `🚀 apollo em execução` / `✅ apollo concluiu` (or the aggregate `✅ N agentes concluídos (...)` for 3+ agents completing within 6s)
|
|
789
|
+
- An honest append to `logs/agent-sessions/delegations.log`: the **real agent name** (extracted from `tool_input.subagent_type`, never `unknown` when present) and a **non-fabricated status** — `success` only on explicit completion evidence, `failure` for refusals/errors, `unknown` otherwise
|
|
790
|
+
- With `export PANTHEON_TOASTS=off` before starting OpenCode: no toasts/reminders at all
|
|
753
791
|
|
|
754
792
|
### Pre-commit hooks (local secret gate)
|
|
755
793
|
|
package/bin/pantheon-init.mjs
CHANGED
|
@@ -37,6 +37,8 @@ function printUsage() {
|
|
|
37
37
|
console.log(' npx pantheon-opencode init --preset <name> # Install and activate model preset');
|
|
38
38
|
console.log(' npx pantheon-opencode set-tier <name> # Set active model preset (global)');
|
|
39
39
|
console.log(' npx pantheon-opencode set-tier <name> --project # Set active model preset (project)');
|
|
40
|
+
console.log(' npx pantheon-opencode prune # List legacy artifacts (dry-run)');
|
|
41
|
+
console.log(' npx pantheon-opencode prune --apply # Remove stale backups');
|
|
40
42
|
console.log(' npx pantheon-opencode --help # Show this help');
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -44,6 +46,18 @@ async function main() {
|
|
|
44
46
|
const args = process.argv.slice(2);
|
|
45
47
|
const command = args[0];
|
|
46
48
|
|
|
49
|
+
// prune handles its own --help (delegated to scripts/prune.mjs), so its
|
|
50
|
+
// dispatch must run BEFORE the generic --help check.
|
|
51
|
+
if (command === 'prune') {
|
|
52
|
+
const { spawnSync } = await import('child_process');
|
|
53
|
+
const pruneScript = path.join(ROOT, 'scripts', 'prune.mjs');
|
|
54
|
+
const result = spawnSync(process.execPath, [pruneScript, ...args.slice(1)], {
|
|
55
|
+
stdio: 'inherit',
|
|
56
|
+
cwd: ROOT,
|
|
57
|
+
});
|
|
58
|
+
process.exit(result.status ?? 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
if (args.includes('--help')) { printUsage(); process.exit(0); }
|
|
48
62
|
|
|
49
63
|
if (command === 'set-tier') {
|
package/opencode.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"instructions": ["AGENTS.md", "src/instructions/*.instructions.md"],
|
|
32
|
-
"plugin": [],
|
|
32
|
+
"plugin": ["/home/ils15/pantheon/src/plugins/pantheon-hooks.ts"],
|
|
33
33
|
"mcp": {
|
|
34
34
|
"bifrost": {
|
|
35
35
|
"type": "remote",
|
|
@@ -46,6 +46,22 @@
|
|
|
46
46
|
},
|
|
47
47
|
"hephaestus": {
|
|
48
48
|
"model": "opencode-go/deepseek-v4-pro"
|
|
49
|
+
},
|
|
50
|
+
"talos": {
|
|
51
|
+
"permission": {
|
|
52
|
+
"edit": {
|
|
53
|
+
"*": "allow",
|
|
54
|
+
"**/migrations/**": "deny",
|
|
55
|
+
"**/alembic/**": "deny",
|
|
56
|
+
"**/models/**": "deny",
|
|
57
|
+
"**/auth/**": "deny",
|
|
58
|
+
"**/*.sql": "deny"
|
|
59
|
+
},
|
|
60
|
+
"bash": {
|
|
61
|
+
"*": "allow",
|
|
62
|
+
"alembic *": "deny"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"postinstall": "node scripts/postinstall.mjs",
|
|
11
|
+
"prepare": "husky",
|
|
11
12
|
"setup": "node bin/pantheon-init.mjs init",
|
|
12
13
|
"setup:lite": "node bin/pantheon-init.mjs init --clean --components agents,skills,instructions",
|
|
13
14
|
"doctor": "node scripts/doctor.mjs",
|
|
15
|
+
"prune": "node scripts/prune.mjs",
|
|
14
16
|
"init": "node bin/pantheon-init.mjs init",
|
|
15
17
|
"opencode": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
|
|
16
18
|
"start": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
"postpublish": "node -e \"console.log('✅ Published pantheon-opencode v' + require('./package.json').version + ' to npm')\"",
|
|
19
21
|
"version:status": "node scripts/versioning.mjs status",
|
|
20
22
|
"version:apply": "node scripts/versioning.mjs apply",
|
|
23
|
+
"version:check": "node scripts/version-check.mjs",
|
|
21
24
|
"release:dry-run": "node scripts/versioning.mjs status && npm pack --dry-run",
|
|
22
25
|
"publish:next": "npm publish --tag next --access public",
|
|
23
26
|
"lint": "npx biome ci .",
|
|
@@ -26,7 +29,8 @@
|
|
|
26
29
|
"secret-scan": "node scripts/secret-scan.mjs",
|
|
27
30
|
"hooks:install": "pre-commit install",
|
|
28
31
|
"hooks:update": "pre-commit autoupdate",
|
|
29
|
-
"test:security": "npm run secret-scan && node tests/test_install_mcp_vision.mjs",
|
|
32
|
+
"test:security": "npm run secret-scan && node tests/test_install_mcp_vision.mjs && node tests/test_doctor_layers.mjs",
|
|
33
|
+
"test:node": "node --test tests/*.mjs",
|
|
30
34
|
"test": "python3 -m pytest tests/ -q 2>&1 || echo \"No tests found\"",
|
|
31
35
|
"test:ci": "python3 -m pytest tests/ -q --tb=short 2>&1 || true"
|
|
32
36
|
},
|
|
@@ -55,6 +59,8 @@
|
|
|
55
59
|
"src/mcp/*.sh",
|
|
56
60
|
"src/routing.yml",
|
|
57
61
|
"src/pantheon/**",
|
|
62
|
+
"src/plugins/**",
|
|
63
|
+
"scripts/hooks/**",
|
|
58
64
|
"src/mcp/requirements*",
|
|
59
65
|
"src/plugins/tui/**",
|
|
60
66
|
"src/skills/**"
|
|
@@ -66,13 +72,16 @@
|
|
|
66
72
|
"@commitlint/cli": "^19.8.0",
|
|
67
73
|
"@commitlint/config-conventional": "^19.8.0",
|
|
68
74
|
"@opencode-ai/plugin": "1.18.11",
|
|
69
|
-
"husky": "^9.1.7"
|
|
70
|
-
"pantheon-opencode": "^1.2.0-beta.9.7bef03e"
|
|
75
|
+
"husky": "^9.1.7"
|
|
71
76
|
},
|
|
72
77
|
"repository": {
|
|
73
78
|
"type": "git",
|
|
74
79
|
"url": "https://github.com/ils15/pantheon-opencode.git"
|
|
75
80
|
},
|
|
81
|
+
"homepage": "https://github.com/ils15/pantheon-opencode",
|
|
82
|
+
"bugs": {
|
|
83
|
+
"url": "https://github.com/ils15/pantheon-opencode/issues"
|
|
84
|
+
},
|
|
76
85
|
"keywords": [
|
|
77
86
|
"github",
|
|
78
87
|
"copilot",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* changelog-extract.mjs — print a version section from CHANGELOG.md
|
|
4
|
+
*
|
|
5
|
+
* The release workflow needs the CHANGELOG entry for the version it is about
|
|
6
|
+
* to tag. This script extracts the body of `## [<version>] - YYYY-MM-DD`
|
|
7
|
+
* (leading `v` tolerated, date optional) and prints it to stdout. When the
|
|
8
|
+
* section is missing it exits 1, so the workflow fails loudly: "add the
|
|
9
|
+
* section in the version-bump PR".
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/changelog-extract.mjs 1.2.1
|
|
13
|
+
* node scripts/changelog-extract.mjs v1.2.1 --changelog /path/to/CHANGELOG.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CHANGELOG = join(dirname(fileURLToPath(import.meta.url)), '..', 'CHANGELOG.md')
|
|
21
|
+
|
|
22
|
+
/** Escape regex metacharacters so a version like 1.0.0 matches literally. */
|
|
23
|
+
function escapeRegExp(text) {
|
|
24
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract the body of a version section from changelog text.
|
|
29
|
+
*
|
|
30
|
+
* Finds the heading `## [<version>] - YYYY-MM-DD` (leading `v` and the date
|
|
31
|
+
* are optional) and returns everything after the heading line up to the next
|
|
32
|
+
* `## [` heading (or end of text). The `## [Unreleased]` heading is never
|
|
33
|
+
* matched, even when asked for explicitly.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} changelogText - full CHANGELOG.md content
|
|
36
|
+
* @param {string} version - e.g. "1.2.1" or "v1.2.1"
|
|
37
|
+
* @returns {{found: boolean, body: string|null}} body is null when not found
|
|
38
|
+
*/
|
|
39
|
+
export function extractSection(changelogText, version) {
|
|
40
|
+
const normalized = String(version).trim().replace(/^v/i, '')
|
|
41
|
+
if (!normalized || /^unreleased$/i.test(normalized)) {
|
|
42
|
+
return { found: false, body: null }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const escaped = escapeRegExp(normalized)
|
|
46
|
+
// Single-line heading: `## [v?1.2.1]` optionally followed by ` - YYYY-MM-DD`.
|
|
47
|
+
// [ \t] (not \s) keeps the match from crossing newlines; the lookahead
|
|
48
|
+
// requires end-of-line or whitespace so `[1.0.0-beta]` can never match `1.0.0`.
|
|
49
|
+
const headingRe = new RegExp(
|
|
50
|
+
`^##[ \\t]+\\[v?${escaped}\\](?:[ \\t]*-[ \\t]*\\d{4}-\\d{2}-\\d{2})?(?=[ \\t]|$)`,
|
|
51
|
+
'm',
|
|
52
|
+
)
|
|
53
|
+
const match = headingRe.exec(changelogText)
|
|
54
|
+
if (!match) return { found: false, body: null }
|
|
55
|
+
|
|
56
|
+
// Advance past the heading line (match.index may point mid-line).
|
|
57
|
+
const newline = changelogText.indexOf('\n', match.index)
|
|
58
|
+
const start = newline === -1 ? changelogText.length : newline + 1
|
|
59
|
+
|
|
60
|
+
// Body ends at the next `## [` heading or end of text.
|
|
61
|
+
const nextHeading = /^##[ \t]+\[/m.exec(changelogText.slice(start))
|
|
62
|
+
const end = nextHeading ? start + nextHeading.index : changelogText.length
|
|
63
|
+
|
|
64
|
+
return { found: true, body: changelogText.slice(start, end).trim() }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
68
|
+
|
|
69
|
+
if (isDirectRun) {
|
|
70
|
+
const args = process.argv.slice(2)
|
|
71
|
+
let changelogPath = DEFAULT_CHANGELOG
|
|
72
|
+
let version = null
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
75
|
+
const arg = args[i]
|
|
76
|
+
if (arg === '--changelog') {
|
|
77
|
+
changelogPath = args[i + 1]
|
|
78
|
+
i += 1
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (arg.startsWith('--changelog=')) {
|
|
82
|
+
changelogPath = arg.slice('--changelog='.length)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
version = arg
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!version) {
|
|
89
|
+
console.error('Usage: node scripts/changelog-extract.mjs <version> [--changelog <path>]')
|
|
90
|
+
process.exitCode = 2
|
|
91
|
+
} else {
|
|
92
|
+
let text
|
|
93
|
+
try {
|
|
94
|
+
text = readFileSync(changelogPath, 'utf-8')
|
|
95
|
+
} catch {
|
|
96
|
+
console.error(`changelog-extract: cannot read ${changelogPath}`)
|
|
97
|
+
process.exitCode = 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (text !== undefined) {
|
|
101
|
+
const { found, body } = extractSection(text, version)
|
|
102
|
+
if (!found) {
|
|
103
|
+
console.error(`changelog-extract: section [${version}] not found in ${changelogPath}`)
|
|
104
|
+
console.error('Add the section in the version-bump PR — the release workflow requires it.')
|
|
105
|
+
process.exitCode = 1
|
|
106
|
+
} else {
|
|
107
|
+
process.stdout.write(`${body}\n`)
|
|
108
|
+
process.exitCode = 0
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|