pirl-framework-cli 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +265 -2
- package/bundled-framework/files/.agents/evals/layer_record.py +438 -0
- package/bundled-framework/files/.agents/evals/tests/test_layer_record.py +264 -0
- package/bundled-framework/files/.agents/evals/tests/test_skills.py +210 -0
- package/bundled-framework/files/.agents/evals/tests/test_validate_layer_artifact.py +214 -0
- package/bundled-framework/files/.agents/evals/validate_layer_artifact.py +415 -0
- package/bundled-framework/files/.agents/instructions/implement.instructions.md +343 -0
- package/bundled-framework/files/.agents/instructions/layered-task-execution.instructions.md +370 -0
- package/bundled-framework/files/.agents/instructions/layout-verification.instructions.md +107 -0
- package/bundled-framework/files/.agents/instructions/plan.instructions.md +292 -0
- package/bundled-framework/files/.agents/instructions/review.instructions.md +236 -0
- package/bundled-framework/files/.agents/instructions/self-evaluation.instructions.md +146 -0
- package/bundled-framework/files/.agents/instructions/standalone-and-maintenance.instructions.md +230 -0
- package/bundled-framework/files/.agents/roles/backend-developer.role.md +45 -0
- package/bundled-framework/files/.agents/roles/business-analyst.role.md +55 -0
- package/bundled-framework/files/.agents/roles/code-analyst.role.md +63 -0
- package/bundled-framework/files/.agents/roles/code-reviewer.role.md +46 -0
- package/bundled-framework/files/.agents/roles/frontend-developer.role.md +51 -0
- package/bundled-framework/files/.agents/roles/process-auditor.role.md +62 -0
- package/bundled-framework/files/.agents/roles/solution-architect.role.md +53 -0
- package/bundled-framework/files/.agents/roles/technical-writer.role.md +68 -0
- package/bundled-framework/files/.agents/roles/ux-designer.role.md +46 -0
- package/bundled-framework/files/.skills/pirl-implement/SKILL.md +115 -0
- package/bundled-framework/files/.skills/pirl-implement/assets/implementation-record-template.md +22 -0
- package/bundled-framework/files/.skills/pirl-implement/references/implementation-record-format.md +41 -0
- package/bundled-framework/files/.skills/pirl-implement/scripts/add_implementation_record.py +58 -0
- package/bundled-framework/files/.skills/pirl-implement/scripts/new_layout_run.py +51 -0
- package/bundled-framework/files/.skills/pirl-plan/SKILL.md +100 -0
- package/bundled-framework/files/.skills/pirl-plan/assets/plan-template.md +48 -0
- package/bundled-framework/files/.skills/pirl-plan/references/plan-format.md +65 -0
- package/bundled-framework/files/.skills/pirl-plan/scripts/new_plan.py +93 -0
- package/bundled-framework/files/.skills/pirl-review/SKILL.md +106 -0
- package/bundled-framework/files/.skills/pirl-review/assets/review-template.md +92 -0
- package/bundled-framework/files/.skills/pirl-review/references/review-format.md +70 -0
- package/bundled-framework/files/.skills/pirl-review/scripts/new_review.py +83 -0
- package/bundled-framework/files/AGENTS.md +303 -0
- package/bundled-framework/files/CONTEXT.md +187 -0
- package/bundled-framework/files/LICENSE +21 -0
- package/bundled-framework/files/README.md +170 -0
- package/bundled-framework/files/gitignore.pirl-bundled +12 -0
- package/bundled-framework/manifest.json +45 -0
- package/dist/bin/main.js +128 -0
- package/dist/bin/pirl.js +6 -0
- package/dist/commands/dispatch.js +57 -0
- package/dist/commands/registry.js +39 -0
- package/dist/core/clipboard.js +70 -0
- package/dist/core/config.js +73 -0
- package/dist/core/exec.js +32 -0
- package/dist/core/paths.js +34 -0
- package/dist/core/targetRepo.js +22 -0
- package/dist/doctor/doctor.js +95 -0
- package/dist/engine/agentMerge.js +71 -0
- package/dist/engine/agents.js +67 -0
- package/dist/engine/attachments.js +251 -0
- package/dist/engine/claudeQuota.js +62 -0
- package/dist/engine/claudeRunner.js +118 -0
- package/dist/engine/claudeStream.js +205 -0
- package/dist/engine/hubData.js +176 -0
- package/dist/engine/permissions.js +67 -0
- package/dist/engine/pricing.js +24 -0
- package/dist/engine/protocol.js +98 -0
- package/dist/engine/run.js +527 -0
- package/dist/engine/runStore.js +67 -0
- package/dist/engine/types.js +3 -0
- package/dist/hub/framework.js +13 -0
- package/dist/hub/hub.js +125 -0
- package/dist/hub/services.js +38 -0
- package/dist/hub/update.js +261 -0
- package/dist/providers/claude.js +62 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/types.js +1 -0
- package/dist/ui/App.js +849 -0
- package/dist/ui/browse.js +144 -0
- package/dist/ui/components/Header.js +54 -0
- package/dist/ui/components/Input.js +76 -0
- package/dist/ui/components/Lines.js +50 -0
- package/dist/ui/components/Panels.js +73 -0
- package/dist/ui/content.js +225 -0
- package/dist/ui/frameworkConsole.js +541 -0
- package/dist/ui/inputEditor.js +86 -0
- package/dist/ui/layout.js +36 -0
- package/dist/ui/lines.js +15 -0
- package/dist/ui/listOverlay.js +97 -0
- package/dist/ui/runView.js +305 -0
- package/dist/ui/session.js +1 -0
- package/dist/ui/terminalInput.js +45 -0
- package/dist/ui/text.js +58 -0
- package/dist/ui/theme.js +139 -0
- package/dist/ui/ticker.js +48 -0
- package/package.json +67 -4
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# PIRL Framework
|
|
2
|
+
|
|
3
|
+
**Plan – Implement – Review Layer**: a Hub that coordinates software development
|
|
4
|
+
work for AI Agents, running entirely on instruction files.
|
|
5
|
+
|
|
6
|
+
You assign work in plain words. The Agent reads [`AGENTS.md`](AGENTS.md), picks
|
|
7
|
+
the right layer, splits the work among specialist roles, records the results as
|
|
8
|
+
files in the repository, then improves the instruction files themselves after
|
|
9
|
+
each run.
|
|
10
|
+
|
|
11
|
+
## What the framework does
|
|
12
|
+
|
|
13
|
+
- **Three layers, three skills.** Plan settles the work to do and the quality
|
|
14
|
+
gates (QG). Implement edits the source according to the Plan. Review checks
|
|
15
|
+
the result against the QGs by reasoning on the source. Each layer is run
|
|
16
|
+
through its skill in `.skills/` (`pirl-plan`, `pirl-implement`, `pirl-review`).
|
|
17
|
+
- **Any repository.** The product repositories are not part of the Hub. You
|
|
18
|
+
name the target repository of each request; the Agent reads that
|
|
19
|
+
repository's own context documentation (its `AGENTS.md`, `CLAUDE.md`, README
|
|
20
|
+
and similar).
|
|
21
|
+
- **AI Agent roles.** Each layer assigns work to roles with the right expertise,
|
|
22
|
+
for example `business-analyst`, `backend-developer`, `code-reviewer`.
|
|
23
|
+
- **Self-improvement.** At the end of each layer run, the `process-auditor` role
|
|
24
|
+
finds each role's mistakes, fixes the relevant instruction files at the root,
|
|
25
|
+
writes a JSONL record and reports what was fixed and where.
|
|
26
|
+
|
|
27
|
+
The framework does not build, does not run the application for acceptance and
|
|
28
|
+
does not deploy. Those belong to your build/test/release stage. Implement only
|
|
29
|
+
writes and runs functional tests related to the request, and captures layout
|
|
30
|
+
with mocks when the UI changes.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- An AI Agent that can read files in the repository and run commands, for
|
|
35
|
+
example Claude Code. If the host loads `AGENTS.md` automatically, it works
|
|
36
|
+
right away; if not, ask the Agent to read `AGENTS.md` before assigning work.
|
|
37
|
+
- Git.
|
|
38
|
+
- Python 3.9 or later, for the validator, the record tool and the skills' scripts.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
### 1. Get the Hub
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
git clone https://github.com/tdmadc107/PIRL-framework.git
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Have a target repository ready
|
|
49
|
+
|
|
50
|
+
Any local Git repository works; nothing needs registering. Give its path in your
|
|
51
|
+
request, relative to the Hub root or absolute. If you want to keep clones
|
|
52
|
+
inside the Hub, put them in `.repos/`, which Git ignores.
|
|
53
|
+
|
|
54
|
+
The Agent learns the repository from the repository itself: architecture,
|
|
55
|
+
conventions, the command that runs tests by file or name, UI languages and
|
|
56
|
+
viewports, known gaps. If the repository has no such document yet, you can
|
|
57
|
+
assign the Agent to write one (for example its `AGENTS.md`) as a standalone
|
|
58
|
+
task.
|
|
59
|
+
|
|
60
|
+
### 3. Assign work
|
|
61
|
+
|
|
62
|
+
No special commands or keywords are needed; just say it:
|
|
63
|
+
|
|
64
|
+
| You want to | Example request | The Agent runs |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| Plan | "Create a Plan for `../shop-web`: customers get an email when an order is cancelled" | `pirl-plan` skill |
|
|
67
|
+
| Implement | "Implement Plan T01 in `<package>`" | `pirl-implement` skill |
|
|
68
|
+
| Review | "Review the implementation of Plan T01" | `pirl-review` skill |
|
|
69
|
+
| Fix per a Review | "Fix per report `<report path>`" | `pirl-implement` skill, repair run |
|
|
70
|
+
| Small work outside the chain | "Update the AGENTS.md of shop-web" | Standalone task |
|
|
71
|
+
| Edit the framework itself | "Fix the cleanup rule in the router" | Framework maintenance |
|
|
72
|
+
| Just ask | "Which tasks does Plan T01 depend on?" | Answer, no files created |
|
|
73
|
+
|
|
74
|
+
Each layer runs only when you ask for it; the Agent does not move on to the next
|
|
75
|
+
layer by itself. The Agent picks the smallest team of roles with enough
|
|
76
|
+
expertise. To choose the team yourself, name the roles in your request; the role
|
|
77
|
+
catalog is in the [standalone and maintenance instructions](.agents/instructions/standalone-and-maintenance.instructions.md#role-catalog).
|
|
78
|
+
|
|
79
|
+
Plans and reports go where you say. If you name no location, they are created
|
|
80
|
+
under the Hub root. Inside the Hub, Git ignores Plan packages and their reports
|
|
81
|
+
(directories named `<request-slug>_<YYYYMMDD-HHmmss>/`), because they describe
|
|
82
|
+
your projects.
|
|
83
|
+
|
|
84
|
+
## A full working cycle
|
|
85
|
+
|
|
86
|
+
1. **Plan.** The Agent investigates the source and asks when a product decision
|
|
87
|
+
is missing. The result is a Plan package `<request-slug>_<YYYYMMDD-HHmmss>/`,
|
|
88
|
+
one file per task with: What to do (REQs), How to do it, Participating roles,
|
|
89
|
+
Required quality gates (QGs). Read and settle the Plan before implementing.
|
|
90
|
+
2. **Implement.** The Agent edits the source in the target repository, within
|
|
91
|
+
the Plan's scope. Verification roles conclude each QG by reasoning on the
|
|
92
|
+
source. The results go into the `Implementation Record` section at the end of
|
|
93
|
+
the Plan file. When the UI changes, layout screenshots are in
|
|
94
|
+
`.tmp/layout-snapshots/` (not in git).
|
|
95
|
+
3. **Review.** The Agent checks each QG against the source and writes a report
|
|
96
|
+
`<plan-set>/<task>/round-<NN>-review-...md`; with the default location it
|
|
97
|
+
sits inside the Plan package. The report has two sections: `Business` (a
|
|
98
|
+
PASS, FAIL or BLOCKED conclusion with FINDs, RISKs and BLOCKs) and
|
|
99
|
+
`Evaluations` (an assessment of each role of the Implement run).
|
|
100
|
+
4. **Fix.** When Business is FAIL, assign "Fix per report ...". Implement runs a
|
|
101
|
+
repair run with the team named in the report, then you ask for a new Review
|
|
102
|
+
round.
|
|
103
|
+
5. **Commit.** The Agent commits or pushes only when you ask. Changes in the
|
|
104
|
+
target repository are committed in that repository; a Hub commit carries the
|
|
105
|
+
framework improvements of the same run. Plans, reports and records stay on
|
|
106
|
+
your machine.
|
|
107
|
+
|
|
108
|
+
## Reading the self-improvement part after each run
|
|
109
|
+
|
|
110
|
+
At the end of each Plan, Implement or Review run, the Agent's response has:
|
|
111
|
+
|
|
112
|
+
- **Self-improvement:** the record ID, and for each applied improvement: the
|
|
113
|
+
issue, cause, file and section edited, old → new version. Look at the diff of
|
|
114
|
+
these files before committing.
|
|
115
|
+
- **Needs confirmation:** the closing block of the answer; it has a point about
|
|
116
|
+
this improvement only when the improvement would loosen a safety rule. You
|
|
117
|
+
choose to apply, skip, or have it re-evaluated by a separate agent; the
|
|
118
|
+
details of the issue and the change are in the "Self-improvement" section.
|
|
119
|
+
The evaluation is always delegated to a separate agent; only when the host
|
|
120
|
+
cannot create agents does the Agent evaluate itself and state the reason.
|
|
121
|
+
|
|
122
|
+
Safety rules (commit, push, secrets, destructive actions, points that need your
|
|
123
|
+
decision) are never loosened by self-improvement without your consent. The full
|
|
124
|
+
definition is in [`AGENTS.md`](AGENTS.md#safety-rules-and-self-improvement).
|
|
125
|
+
|
|
126
|
+
Records live in `.agents/evals/records/<layer>/<YYYY-MM-DD>.jsonl`. They
|
|
127
|
+
describe your projects, so Git ignores that directory: records stay on your
|
|
128
|
+
machine and the tool still reads them. To look up history:
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
python -B .agents/evals/layer_record.py query --issue-key <issue key>
|
|
132
|
+
python -B .agents/evals/layer_record.py query --layer review --plan-set <package> --task T01
|
|
133
|
+
python -B .agents/evals/layer_record.py check
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Checks after editing the framework
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
python -B -m unittest discover -s .agents/evals/tests -p "test_*.py"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Pick the check commands that match what you edited: see
|
|
143
|
+
[Framework checks](.agents/instructions/standalone-and-maintenance.instructions.md#framework-checks).
|
|
144
|
+
|
|
145
|
+
## Directory structure
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
AGENTS.md root: core rules, flow, choosing the kind of work, delegation, target repository
|
|
149
|
+
CONTEXT.md how responses to the user are presented
|
|
150
|
+
.agents/ layer instructions, roles, evals (validator, record tool, tests, local records)
|
|
151
|
+
.skills/ the pirl-plan, pirl-implement and pirl-review skills
|
|
152
|
+
.tmp/ temporary output, not in git
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Full tree: [framework structure](.agents/instructions/standalone-and-maintenance.instructions.md#framework-structure).
|
|
156
|
+
|
|
157
|
+
## Reference documentation
|
|
158
|
+
|
|
159
|
+
| To learn about | Read |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| Core rules, flow, delegation, target repository | [`AGENTS.md`](AGENTS.md) |
|
|
162
|
+
| How the Agent presents answers, summaries, proposals | [`CONTEXT.md`](CONTEXT.md) |
|
|
163
|
+
| Choosing a layer, shared contracts, functional tests, self-improvement | [Router](.agents/instructions/layered-task-execution.instructions.md) |
|
|
164
|
+
| Each layer's decisions | [Plan](.agents/instructions/plan.instructions.md), [Implement](.agents/instructions/implement.instructions.md), [Review](.agents/instructions/review.instructions.md) |
|
|
165
|
+
| Running each layer, file formats, templates | [`pirl-plan`](.skills/pirl-plan/SKILL.md), [`pirl-implement`](.skills/pirl-implement/SKILL.md), [`pirl-review`](.skills/pirl-review/SKILL.md) |
|
|
166
|
+
| Each role's expertise | [`.agents/roles/`](.agents/roles/) |
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
.tmp/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.stackdump
|
|
4
|
+
|
|
5
|
+
# External local tools: kept on the local machine, not part of the framework.
|
|
6
|
+
.tools/
|
|
7
|
+
|
|
8
|
+
# Private data about the user's projects: kept on the local machine, never committed.
|
|
9
|
+
.agents/evals/records/
|
|
10
|
+
.repos/
|
|
11
|
+
# Plan packages <request-slug>_<YYYYMMDD-HHmmss>/ and the Review reports inside them, anywhere in the Hub.
|
|
12
|
+
*_[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.1.0",
|
|
3
|
+
"digest": "7826b2333a1d935560c750991fff036ffaa9b204",
|
|
4
|
+
"files": {
|
|
5
|
+
".agents/evals/layer_record.py": "e9dc56cf38b3723aed3c3f22a2a8c0d0c3dfa32c",
|
|
6
|
+
".agents/evals/tests/test_layer_record.py": "ed6bca11e9103c6be9a1436bc764a2fd14639e75",
|
|
7
|
+
".agents/evals/tests/test_skills.py": "43ae9a91adc7a338f3609e32edffa8ae2d39ed3f",
|
|
8
|
+
".agents/evals/tests/test_validate_layer_artifact.py": "013445c684ebc38dfbba919fb234a1a8984c1213",
|
|
9
|
+
".agents/evals/validate_layer_artifact.py": "cef4fe2a85a983828f201a5ff9ce74e448a1bfc7",
|
|
10
|
+
".agents/instructions/implement.instructions.md": "d168b1c2d0d229047561327091bc143a65924744",
|
|
11
|
+
".agents/instructions/layered-task-execution.instructions.md": "bebc5f25f6b0b409a1054b43f8d8ede2f6df9b24",
|
|
12
|
+
".agents/instructions/layout-verification.instructions.md": "746abd0793ec096c2d69501e00a16b3b6433aad4",
|
|
13
|
+
".agents/instructions/plan.instructions.md": "59358acd397653f71a5f15bc1c2be0ccdf6045c9",
|
|
14
|
+
".agents/instructions/review.instructions.md": "ba0fd8a558d5c44947189f17e4d430c1a3eb0982",
|
|
15
|
+
".agents/instructions/self-evaluation.instructions.md": "280c5ec812fd61ed31809037dfcd2d593cc47c1a",
|
|
16
|
+
".agents/instructions/standalone-and-maintenance.instructions.md": "15a96334813b1d74f824118c70a18f0b6712dbef",
|
|
17
|
+
".agents/roles/backend-developer.role.md": "b41ae3aa5f930369f995293dabce797b58cc3688",
|
|
18
|
+
".agents/roles/business-analyst.role.md": "6552002bff81ca4da70da9d05145c49154f23fcf",
|
|
19
|
+
".agents/roles/code-analyst.role.md": "d171fb0388ef2b564d6994baaa0ca9b450686121",
|
|
20
|
+
".agents/roles/code-reviewer.role.md": "8f7053eff48ac246cef36bd9fa6dab03eca88f4d",
|
|
21
|
+
".agents/roles/frontend-developer.role.md": "5feffc5035281e787c2d1f6b4bf53adc017b5601",
|
|
22
|
+
".agents/roles/process-auditor.role.md": "896f30f77a4d07d31258f13d65e81e867914a30e",
|
|
23
|
+
".agents/roles/solution-architect.role.md": "48855ae414d3af4ca8bf42f99cbd8ad5d5e73da5",
|
|
24
|
+
".agents/roles/technical-writer.role.md": "aff96083ffe43a11395b1277c208aa991b74bdd3",
|
|
25
|
+
".agents/roles/ux-designer.role.md": "349268f8817a36facf4a854daaa571d3fb62022d",
|
|
26
|
+
".gitignore": "9e417338f1bd7356a16e4e9de85ee6b3ceadc315",
|
|
27
|
+
".skills/pirl-implement/SKILL.md": "da0931090bc11292fa41c0122c31cf4749d80d5c",
|
|
28
|
+
".skills/pirl-implement/assets/implementation-record-template.md": "66bfc2333f1bcf7a8b4dd3394f2ebb8a960cfccd",
|
|
29
|
+
".skills/pirl-implement/references/implementation-record-format.md": "bad5b435a8ac4ed28a9d9c933549cb413ad2bfdc",
|
|
30
|
+
".skills/pirl-implement/scripts/add_implementation_record.py": "43a5dcdefd24c9dcac4930a26fb842dea1ed9ded",
|
|
31
|
+
".skills/pirl-implement/scripts/new_layout_run.py": "fbfd0f44b09200b4d7718aed5953680cc7ea45a3",
|
|
32
|
+
".skills/pirl-plan/SKILL.md": "27b4903bfb12418c465e0b45f9b70e740ae7b1e2",
|
|
33
|
+
".skills/pirl-plan/assets/plan-template.md": "560bf8b8dca37d1b203b2c4cd643007c688dc10e",
|
|
34
|
+
".skills/pirl-plan/references/plan-format.md": "53d518428920b332b6678310105d3c3a48979f0d",
|
|
35
|
+
".skills/pirl-plan/scripts/new_plan.py": "ac949673d7c7a779200009dae2992d25381b8e5d",
|
|
36
|
+
".skills/pirl-review/SKILL.md": "35c8308aa6e4c18047953ccad5ff41172461d943",
|
|
37
|
+
".skills/pirl-review/assets/review-template.md": "1f27f7082d01c8eb4de94f9693913baf7aad64a2",
|
|
38
|
+
".skills/pirl-review/references/review-format.md": "5a24c923a7d517bd4d1f42d0dd5907aac81a72a9",
|
|
39
|
+
".skills/pirl-review/scripts/new_review.py": "27150e6a6e249507c839a05a7f337380a062b1c1",
|
|
40
|
+
"AGENTS.md": "ba49644c3d519843cd7dc97d6d8a52ca8bd5d23e",
|
|
41
|
+
"CONTEXT.md": "2779b5131d1ff7e8963c2516709d1a40c341823e",
|
|
42
|
+
"LICENSE": "f5a48e5836ce6a46ffcf13a8240a954b244b90ae",
|
|
43
|
+
"README.md": "d53ba7374742fae5e519bbc7953878ad858f7ca6"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/bin/main.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { createClipboard } from '../core/clipboard.js';
|
|
5
|
+
import { loadConfig, saveConfig } from '../core/config.js';
|
|
6
|
+
import { exec } from '../core/exec.js';
|
|
7
|
+
import { configFile, displayPath, hubDir, packageInfo, packageRoot, pirlHome } from '../core/paths.js';
|
|
8
|
+
import { detectTargetRepo } from '../core/targetRepo.js';
|
|
9
|
+
import { runDoctor } from '../doctor/doctor.js';
|
|
10
|
+
import { readClaudeQuota } from '../engine/claudeQuota.js';
|
|
11
|
+
import { attachmentContext, pathIndex, resolveMentions } from '../engine/attachments.js';
|
|
12
|
+
import { createClaudeRunner } from '../engine/claudeRunner.js';
|
|
13
|
+
import { readPlanPackages, readRecords } from '../engine/hubData.js';
|
|
14
|
+
import { RunController } from '../engine/run.js';
|
|
15
|
+
import { listRuns, saveRun } from '../engine/runStore.js';
|
|
16
|
+
import { loadBundledFramework } from '../hub/framework.js';
|
|
17
|
+
import { frameworkServices } from '../hub/services.js';
|
|
18
|
+
import { ensureHub } from '../hub/hub.js';
|
|
19
|
+
import { getProvider } from '../providers/index.js';
|
|
20
|
+
import { App } from '../ui/App.js';
|
|
21
|
+
import { MOUSE_OFF, mouseSequence } from '../ui/terminalInput.js';
|
|
22
|
+
/** The run's model settings as the provider's arguments; `default` and `inherit` leave the choice to it. */
|
|
23
|
+
export const runModels = (config) => ({
|
|
24
|
+
model: config.model === 'default' ? undefined : config.model,
|
|
25
|
+
roleModel: config.roleModel === 'inherit' ? undefined : config.roleModel,
|
|
26
|
+
effort: config.effort === 'default' ? undefined : config.effort,
|
|
27
|
+
});
|
|
28
|
+
const HELP = `PIRL CLI: chạy PIRL framework (Plan – Implement – Review) bằng agent CLI bạn đã đăng nhập.
|
|
29
|
+
|
|
30
|
+
Cách dùng
|
|
31
|
+
pirl Mở TUI. Thư mục hiện tại (git repo) là target repo.
|
|
32
|
+
pirl --version In phiên bản CLI và framework đi kèm.
|
|
33
|
+
pirl --help In hướng dẫn này.
|
|
34
|
+
|
|
35
|
+
Trong TUI, gõ / để xem lệnh, hoặc /help để xem lệnh và phím tắt.`;
|
|
36
|
+
async function main() {
|
|
37
|
+
const root = packageRoot();
|
|
38
|
+
const pkg = packageInfo(root);
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
41
|
+
console.log(HELP);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
45
|
+
let framework = 'không có';
|
|
46
|
+
try {
|
|
47
|
+
framework = `fw-${loadBundledFramework(root).manifest.version}`;
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
console.log(`pirl ${pkg.version} (framework ${framework})`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (args.length > 0) {
|
|
54
|
+
console.error(`pirl: không nhận tham số "${args[0]}". Lệnh chạy bên trong TUI.\n\n${HELP}`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
58
|
+
console.error('pirl cần chạy trong một terminal tương tác.');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
const home = pirlHome();
|
|
62
|
+
const notes = [];
|
|
63
|
+
const { config, warning } = loadConfig(configFile(home));
|
|
64
|
+
if (warning)
|
|
65
|
+
notes.push({ tone: 'warning', text: warning });
|
|
66
|
+
const target = await detectTargetRepo(process.cwd());
|
|
67
|
+
if (target.kind === 'none') {
|
|
68
|
+
notes.push({ tone: 'warning', text: `${target.reason}: các layer bị khoá.`, hint: 'Mở `pirl` trong thư mục của một git repo để dùng /pirl-plan, /pirl-implement, /pirl-review.' });
|
|
69
|
+
}
|
|
70
|
+
let bundle = null;
|
|
71
|
+
let hub = null;
|
|
72
|
+
let hubError = null;
|
|
73
|
+
try {
|
|
74
|
+
bundle = loadBundledFramework(root);
|
|
75
|
+
const result = await ensureHub(hubDir(home), bundle);
|
|
76
|
+
hub = result.status;
|
|
77
|
+
if (result.created)
|
|
78
|
+
notes.push({ tone: 'success', text: `Đã tạo Hub tại ${displayPath(hub.path)} với framework ${hub.upstreamTag ?? ''}.` });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
hubError = error instanceof Error ? error.message : String(error);
|
|
82
|
+
notes.push({ tone: 'error', text: `Không chuẩn bị được Hub: ${hubError}` });
|
|
83
|
+
}
|
|
84
|
+
const targetRoot = target.kind === 'git' ? target.root : null;
|
|
85
|
+
// `@` mentions are read from the target repository, or from the start directory outside one.
|
|
86
|
+
const attachBase = targetRoot ?? process.cwd();
|
|
87
|
+
const paths = cachedPaths(() => pathIndex(attachBase));
|
|
88
|
+
const runner = createClaudeRunner();
|
|
89
|
+
const instance = render(_jsx(App, { info: { cliVersion: pkg.version, target, hub, hubError }, initialConfig: config, startupNotes: notes, services: {
|
|
90
|
+
runDoctor: current => runDoctor({ exec, provider: getProvider(current.provider), target, hub, hubError }),
|
|
91
|
+
saveConfig: next => saveConfig(configFile(home), next),
|
|
92
|
+
startRun: options => new RunController({ runner, hub: hub.path, target: targetRoot, logDir: join(home, 'logs'), quota: () => readClaudeQuota('claude', hub.path), python: options.python, maxParallelRoles: options.config.maxParallelRoles, recentOutputs: options.recentOutputs, attachments: options.attachments, ...runModels(options.config) }, options.id, options.kind, options.request, options.resume),
|
|
93
|
+
saveRun: run => targetRoot && saveRun(home, targetRoot, run),
|
|
94
|
+
runs: () => (targetRoot ? listRuns(home, targetRoot) : []),
|
|
95
|
+
plans: () => (hub && targetRoot ? readPlanPackages(hub.path, targetRoot) : []),
|
|
96
|
+
records: () => (hub ? readRecords(hub.path, targetRoot) : []),
|
|
97
|
+
clipboard: createClipboard({ writeTerminal: data => process.stdout.write(data) }),
|
|
98
|
+
quota: () => (hub ? readClaudeQuota('claude', hub.path) : Promise.resolve(null)),
|
|
99
|
+
paths: () => paths.get(),
|
|
100
|
+
attach: text => attachmentContext(resolveMentions(text, attachBase, process.cwd()), [attachBase, ...(hub ? [hub.path] : [])]),
|
|
101
|
+
setMouse: mode => process.stdout.write(mouseSequence(mode)),
|
|
102
|
+
framework: hub && bundle ? frameworkServices(hub.path, bundle, runner, config => runModels(config).model) : undefined,
|
|
103
|
+
} }),
|
|
104
|
+
// Ctrl+C is handled by the TUI: it stops a running run first. The kitty keyboard protocol, where the
|
|
105
|
+
// terminal supports it (Windows Terminal 1.25+, kitty, WezTerm, Ghostty, iTerm2), tells Shift+Enter from Enter.
|
|
106
|
+
// Incremental rendering rewrites only the rows that changed: a spinner tick sends a few bytes, not a screen.
|
|
107
|
+
{ alternateScreen: true, exitOnCtrlC: false, kittyKeyboard: { mode: 'auto' }, incrementalRendering: true });
|
|
108
|
+
// The terminal keeps reporting the mouse after the TUI ends unless it is told to stop, even on a crash.
|
|
109
|
+
process.once('exit', () => process.stdout.write(MOUSE_OFF));
|
|
110
|
+
await instance.waitUntilExit();
|
|
111
|
+
process.stdout.write(MOUSE_OFF);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
main().then(code => process.exit(code), error => {
|
|
115
|
+
console.error(error instanceof Error ? error.stack : error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
118
|
+
/** The path index for `@` completion, read again when older than a few seconds so new files show up. */
|
|
119
|
+
function cachedPaths(read, maxAgeMs = 10_000) {
|
|
120
|
+
let cached = null;
|
|
121
|
+
return {
|
|
122
|
+
get() {
|
|
123
|
+
if (!cached || Date.now() - cached.at > maxAgeMs)
|
|
124
|
+
cached = { at: Date.now(), value: read().catch(() => []) };
|
|
125
|
+
return cached.value;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/bin/pirl.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// React and Ink run their development builds unless NODE_ENV says otherwise; the development build checks
|
|
3
|
+
// every render and costs several times the CPU. Set it before they load, which is why the TUI is imported here.
|
|
4
|
+
process.env.NODE_ENV ??= 'production';
|
|
5
|
+
await import('./main.js');
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { commandUsage } from './registry.js';
|
|
2
|
+
const LAYERS = { 'pirl-plan': 'plan', 'pirl-implement': 'implement', 'pirl-review': 'review' };
|
|
3
|
+
const LABELS = { plan: 'Plan', implement: 'Implement', review: 'Review' };
|
|
4
|
+
export function dispatch(input, context) {
|
|
5
|
+
switch (input.kind) {
|
|
6
|
+
case 'empty':
|
|
7
|
+
return [];
|
|
8
|
+
case 'unknown-command':
|
|
9
|
+
return [{ type: 'note', tone: 'error', text: `Không có lệnh /${input.name}.`, hint: 'Gõ /help để xem danh sách lệnh.' }];
|
|
10
|
+
case 'text':
|
|
11
|
+
// The coordinator classifies free text (question, standalone task, framework maintenance) per AGENTS.md.
|
|
12
|
+
return [{ type: 'run', kind: 'chat', request: input.text }];
|
|
13
|
+
case 'command':
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
const { spec, request } = input;
|
|
17
|
+
if (spec.name in LAYERS) {
|
|
18
|
+
const kind = LAYERS[spec.name];
|
|
19
|
+
if (context.target.kind !== 'git') {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
type: 'note',
|
|
23
|
+
tone: 'error',
|
|
24
|
+
text: `Layer ${LABELS[kind]} bị khoá: ${context.target.reason.toLowerCase()}.`,
|
|
25
|
+
hint: 'Thoát (/exit), chuyển tới thư mục của một git repo rồi gõ lại `pirl`.',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
if (!request)
|
|
30
|
+
return [{ type: 'note', tone: 'warning', text: 'Thiếu nội dung request.', hint: `Cú pháp: ${commandUsage(spec)}` }];
|
|
31
|
+
return [{ type: 'run', kind, request }];
|
|
32
|
+
}
|
|
33
|
+
switch (spec.name) {
|
|
34
|
+
case 'help':
|
|
35
|
+
return [{ type: 'overlay', overlay: 'help' }];
|
|
36
|
+
case 'doctor':
|
|
37
|
+
return [{ type: 'overlay', overlay: 'doctor' }, { type: 'doctor' }];
|
|
38
|
+
case 'settings':
|
|
39
|
+
return [{ type: 'overlay', overlay: 'settings' }];
|
|
40
|
+
case 'framework':
|
|
41
|
+
return [{ type: 'overlay', overlay: 'framework' }];
|
|
42
|
+
case 'plans':
|
|
43
|
+
return [{ type: 'overlay', overlay: 'plans' }];
|
|
44
|
+
case 'records':
|
|
45
|
+
return [{ type: 'overlay', overlay: 'records' }];
|
|
46
|
+
case 'resume':
|
|
47
|
+
return [{ type: 'resume' }];
|
|
48
|
+
case 'copy':
|
|
49
|
+
return [{ type: 'copy' }];
|
|
50
|
+
case 'clear':
|
|
51
|
+
return [{ type: 'clear' }];
|
|
52
|
+
case 'exit':
|
|
53
|
+
return [{ type: 'exit' }];
|
|
54
|
+
default:
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const COMMANDS = [
|
|
2
|
+
{ name: 'pirl-plan', group: 'Layer', summary: 'Tạo hoặc điều chỉnh Plan', takesRequest: true },
|
|
3
|
+
{ name: 'pirl-implement', group: 'Layer', summary: 'Thực hiện một task của Plan, hoặc sửa theo report', takesRequest: true },
|
|
4
|
+
{ name: 'pirl-review', group: 'Layer', summary: 'Review kết quả implement của một task', takesRequest: true },
|
|
5
|
+
{ name: 'resume', group: 'Run', summary: 'Chạy tiếp run bị dừng', takesRequest: false },
|
|
6
|
+
{ name: 'plans', group: 'Tra cứu', summary: 'Danh sách Plan, task, report của repo này', takesRequest: false },
|
|
7
|
+
{ name: 'records', group: 'Tra cứu', summary: 'Tra cứu record tự cải tiến', takesRequest: false },
|
|
8
|
+
{ name: 'settings', group: 'Cấu hình', summary: 'Provider, giao diện, số role chạy song song', takesRequest: false },
|
|
9
|
+
{ name: 'doctor', group: 'Cấu hình', summary: 'Kiểm tra provider, Python, Git, Hub', takesRequest: false },
|
|
10
|
+
{ name: 'framework', group: 'Framework', summary: 'Phiên bản framework, cải tiến cục bộ, cập nhật', takesRequest: false },
|
|
11
|
+
{ name: 'copy', group: 'Phiên', summary: 'Copy câu trả lời gần nhất của PIRL', takesRequest: false },
|
|
12
|
+
{ name: 'clear', group: 'Phiên', summary: 'Xoá khung chat, bắt đầu hội thoại mới', takesRequest: false },
|
|
13
|
+
{ name: 'help', group: 'Phiên', summary: 'Danh sách lệnh và phím tắt', takesRequest: false },
|
|
14
|
+
{ name: 'exit', group: 'Phiên', summary: 'Thoát PIRL', takesRequest: false },
|
|
15
|
+
];
|
|
16
|
+
export const commandUsage = (spec) => `/${spec.name}${spec.takesRequest ? ' <request>' : ''}`;
|
|
17
|
+
export function parseInput(raw) {
|
|
18
|
+
const text = raw.trim();
|
|
19
|
+
if (!text)
|
|
20
|
+
return { kind: 'empty' };
|
|
21
|
+
if (!text.startsWith('/'))
|
|
22
|
+
return { kind: 'text', text };
|
|
23
|
+
const match = /^\/(\S*)\s*([\s\S]*)$/.exec(text);
|
|
24
|
+
const name = match[1].toLowerCase();
|
|
25
|
+
const spec = COMMANDS.find(command => command.name === name);
|
|
26
|
+
return spec ? { kind: 'command', spec, request: match[2].trim() } : { kind: 'unknown-command', name };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Commands offered while the user types a command name: input starting with "/" and no space yet.
|
|
30
|
+
* Prefix matches come first, then commands that contain the typed text elsewhere (so "/plan" finds /pirl-plan).
|
|
31
|
+
*/
|
|
32
|
+
export function suggestCommands(input) {
|
|
33
|
+
if (!input.startsWith('/') || /\s/.test(input))
|
|
34
|
+
return [];
|
|
35
|
+
const typed = input.slice(1).toLowerCase();
|
|
36
|
+
const prefix = COMMANDS.filter(command => command.name.startsWith(typed));
|
|
37
|
+
const inside = COMMANDS.filter(command => !command.name.startsWith(typed) && command.name.includes(typed));
|
|
38
|
+
return [...prefix, ...inside];
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { exec as defaultExec, succeeded } from './exec.js';
|
|
5
|
+
const POWERSHELL_FLAGS = ['-NoProfile', '-NonInteractive', '-Command'];
|
|
6
|
+
function tools(platform, env) {
|
|
7
|
+
const windows = platform === 'win32' || Boolean(env['WSL_DISTRO_NAME']);
|
|
8
|
+
if (windows) {
|
|
9
|
+
const powershell = platform === 'win32' ? 'powershell' : 'powershell.exe';
|
|
10
|
+
return {
|
|
11
|
+
windows,
|
|
12
|
+
copy: [{ command: powershell, args: POWERSHELL_FLAGS }],
|
|
13
|
+
paste: [{ command: powershell, args: [...POWERSHELL_FLAGS, '[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-Clipboard -Raw'] }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (platform === 'darwin')
|
|
17
|
+
return { windows, copy: [{ command: 'pbcopy', args: [] }], paste: [{ command: 'pbpaste', args: [] }] };
|
|
18
|
+
const wayland = env['WAYLAND_DISPLAY'] ? [{ copy: { command: 'wl-copy', args: [] }, paste: { command: 'wl-paste', args: ['--no-newline'] } }] : [];
|
|
19
|
+
const x11 = [
|
|
20
|
+
{ copy: { command: 'xclip', args: ['-selection', 'clipboard'] }, paste: { command: 'xclip', args: ['-selection', 'clipboard', '-o'] } },
|
|
21
|
+
{ copy: { command: 'xsel', args: ['--clipboard', '--input'] }, paste: { command: 'xsel', args: ['--clipboard', '--output'] } },
|
|
22
|
+
];
|
|
23
|
+
const all = [...wayland, ...x11];
|
|
24
|
+
return { windows, copy: all.map(tool => tool.copy), paste: all.map(tool => tool.paste) };
|
|
25
|
+
}
|
|
26
|
+
/** OSC 52 asks the terminal itself to set the clipboard; Windows Terminal, iTerm2, kitty and others honor it. */
|
|
27
|
+
export const osc52 = (text) => `\u001B]52;c;${Buffer.from(text, 'utf8').toString('base64')}\u0007`;
|
|
28
|
+
export function createClipboard(options = {}) {
|
|
29
|
+
const exec = options.exec ?? defaultExec;
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
const found = tools(options.platform ?? process.platform, env);
|
|
32
|
+
const utf8Env = { ...env, LANG: env['LANG'] || 'en_US.UTF-8', LC_CTYPE: env['LC_CTYPE'] || 'en_US.UTF-8' };
|
|
33
|
+
return {
|
|
34
|
+
async copy(text) {
|
|
35
|
+
for (const tool of found.copy) {
|
|
36
|
+
let result;
|
|
37
|
+
if (found.windows) {
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), 'pirl-clip-'));
|
|
39
|
+
const file = join(dir, 'clip.txt');
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(file, text, 'utf8');
|
|
42
|
+
const quoted = file.replace(/'/g, "''");
|
|
43
|
+
result = await exec(tool.command, [...tool.args, `Get-Content -Raw -Encoding UTF8 -LiteralPath '${quoted}' | Set-Clipboard`], { timeoutMs: 15_000 });
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
rmSync(dir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
result = await exec(tool.command, tool.args, { input: text, env: utf8Env, timeoutMs: 15_000 });
|
|
51
|
+
}
|
|
52
|
+
if (succeeded(result))
|
|
53
|
+
return 'native';
|
|
54
|
+
}
|
|
55
|
+
if (options.writeTerminal) {
|
|
56
|
+
options.writeTerminal(osc52(text));
|
|
57
|
+
return 'terminal';
|
|
58
|
+
}
|
|
59
|
+
return 'failed';
|
|
60
|
+
},
|
|
61
|
+
async paste() {
|
|
62
|
+
for (const tool of found.paste) {
|
|
63
|
+
const result = await exec(tool.command, tool.args, { env: utf8Env, timeoutMs: 15_000 });
|
|
64
|
+
if (succeeded(result))
|
|
65
|
+
return found.windows ? result.stdout.replace(/\r\n/g, '\n').replace(/\n$/, '') : result.stdout;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
export const PROVIDER_IDS = ['claude', 'codex', 'gemini', 'copilot'];
|
|
4
|
+
export const COLOR_SCHEME_IDS = ['pirl', 'ocean', 'forest', 'sunset', 'grape', 'mono'];
|
|
5
|
+
/** Model aliases the provider resolves to its newest version; `default` leaves the choice to the provider. */
|
|
6
|
+
export const MODEL_IDS = ['sonnet', 'opus', 'fable', 'haiku', 'default'];
|
|
7
|
+
/** `inherit` runs the roles on the coordinator's model. */
|
|
8
|
+
export const ROLE_MODEL_IDS = ['inherit', 'sonnet', 'opus', 'fable', 'haiku'];
|
|
9
|
+
/**
|
|
10
|
+
* What the mouse does in the TUI:
|
|
11
|
+
* - `select`: the terminal keeps the mouse, so dragging selects text as usual; the wheel reaches the TUI as the
|
|
12
|
+
* arrow keys (alternate scroll mode), which scroll the chat.
|
|
13
|
+
* - `wheel`: the TUI receives the wheel itself; selecting text then needs Shift held while dragging.
|
|
14
|
+
* - `off`: the TUI asks nothing of the mouse.
|
|
15
|
+
*/
|
|
16
|
+
export const MOUSE_IDS = ['select', 'wheel', 'off'];
|
|
17
|
+
export const EFFORT_IDS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
18
|
+
export const DEFAULT_CONFIG = {
|
|
19
|
+
provider: 'claude',
|
|
20
|
+
theme: 'auto',
|
|
21
|
+
colors: 'pirl',
|
|
22
|
+
glyphs: 'auto',
|
|
23
|
+
motion: true,
|
|
24
|
+
mouse: 'select',
|
|
25
|
+
maxParallelRoles: 2,
|
|
26
|
+
model: 'sonnet',
|
|
27
|
+
roleModel: 'inherit',
|
|
28
|
+
effort: 'default',
|
|
29
|
+
};
|
|
30
|
+
export const PARALLEL_LIMITS = { min: 1, max: 4 };
|
|
31
|
+
const oneOf = (value, allowed, fallback) => typeof value === 'string' && allowed.includes(value) ? value : fallback;
|
|
32
|
+
/** Keeps every valid field of a stored config and falls back to the default for the rest. */
|
|
33
|
+
export function normalizeConfig(raw) {
|
|
34
|
+
const data = raw && typeof raw === 'object' ? raw : {};
|
|
35
|
+
const parallel = data['maxParallelRoles'];
|
|
36
|
+
return {
|
|
37
|
+
provider: oneOf(data['provider'], PROVIDER_IDS, DEFAULT_CONFIG.provider),
|
|
38
|
+
theme: oneOf(data['theme'], ['auto', 'dark', 'light'], DEFAULT_CONFIG.theme),
|
|
39
|
+
colors: oneOf(data['colors'], COLOR_SCHEME_IDS, DEFAULT_CONFIG.colors),
|
|
40
|
+
glyphs: oneOf(data['glyphs'], ['auto', 'unicode', 'ascii'], DEFAULT_CONFIG.glyphs),
|
|
41
|
+
motion: typeof data['motion'] === 'boolean' ? data['motion'] : DEFAULT_CONFIG.motion,
|
|
42
|
+
// Earlier versions stored on or off.
|
|
43
|
+
mouse: data['mouse'] === false ? 'off' : oneOf(data['mouse'], MOUSE_IDS, DEFAULT_CONFIG.mouse),
|
|
44
|
+
maxParallelRoles: typeof parallel === 'number' && Number.isInteger(parallel) && parallel >= PARALLEL_LIMITS.min && parallel <= PARALLEL_LIMITS.max
|
|
45
|
+
? parallel
|
|
46
|
+
: DEFAULT_CONFIG.maxParallelRoles,
|
|
47
|
+
model: oneOf(data['model'], MODEL_IDS, DEFAULT_CONFIG.model),
|
|
48
|
+
roleModel: oneOf(data['roleModel'], ROLE_MODEL_IDS, DEFAULT_CONFIG.roleModel),
|
|
49
|
+
effort: oneOf(data['effort'], EFFORT_IDS, DEFAULT_CONFIG.effort),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function loadConfig(file) {
|
|
53
|
+
let text;
|
|
54
|
+
try {
|
|
55
|
+
text = readFileSync(file, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { config: { ...DEFAULT_CONFIG } };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return { config: normalizeConfig(JSON.parse(text)) };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { config: { ...DEFAULT_CONFIG }, warning: `Không đọc được ${file}; đang dùng cài đặt mặc định` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Writes through a temporary file so an interrupted write never leaves a broken config. */
|
|
68
|
+
export function saveConfig(file, config) {
|
|
69
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
70
|
+
const temporary = `${file}.tmp`;
|
|
71
|
+
writeFileSync(temporary, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
72
|
+
renameSync(temporary, file);
|
|
73
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
/** Runs a program without a shell. cross-spawn resolves `.cmd` shims on Windows (claude, npm, ...). */
|
|
3
|
+
export const exec = (command, args, options = {}) => new Promise(resolve => {
|
|
4
|
+
let stdout = '';
|
|
5
|
+
let stderr = '';
|
|
6
|
+
let timedOut = false;
|
|
7
|
+
let settled = false;
|
|
8
|
+
const finish = (result) => {
|
|
9
|
+
if (settled)
|
|
10
|
+
return;
|
|
11
|
+
settled = true;
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
resolve(result);
|
|
14
|
+
};
|
|
15
|
+
const child = spawn(command, args, {
|
|
16
|
+
cwd: options.cwd,
|
|
17
|
+
env: options.env ?? process.env,
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
timedOut = true;
|
|
23
|
+
child.kill();
|
|
24
|
+
}, options.timeoutMs ?? 30_000);
|
|
25
|
+
child.stdout?.setEncoding('utf8').on('data', (chunk) => (stdout += chunk));
|
|
26
|
+
child.stderr?.setEncoding('utf8').on('data', (chunk) => (stderr += chunk));
|
|
27
|
+
child.on('error', error => finish({ code: null, stdout, stderr, error: error.message }));
|
|
28
|
+
child.on('close', code => finish({ code: timedOut ? null : code, stdout, stderr, timedOut }));
|
|
29
|
+
child.stdin?.on('error', () => { });
|
|
30
|
+
child.stdin?.end(options.input ?? '');
|
|
31
|
+
});
|
|
32
|
+
export const succeeded = (result) => result.code === 0 && !result.error;
|