@piagent/platform 1.1.4 → 1.1.6
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 +34 -0
- package/README.md +1 -1
- package/adapters/be-readonly-fe/profile.json +12 -4
- package/docs/command-reference-vietnamese.md +6 -3
- package/docs/distribution-standard.md +7 -7
- package/docs/operator-manual-vietnamese.md +19 -7
- package/docs/project-adapters.md +21 -0
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +19 -7
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +14 -3
- package/docs-site/index.html +2 -2
- package/package.json +2 -1
- package/packages/piagent-core/capabilities/capability-core.js +5 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +1 -69
- package/packages/piagent-core/extensions/project-shape.js +285 -0
- package/packages/piagent-core/package.json +1 -1
- package/scripts/init-project.sh +12 -89
- package/scripts/install-global.sh +3 -3
- package/scripts/piagent-cli.mjs +1 -0
- package/scripts/team-doctor.sh +1 -1
- package/scripts/update-global.mjs +265 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
This file records release-facing changes for Pi Agent Platform. Copy the relevant version block into GitHub Releases when publishing a tag.
|
|
4
4
|
|
|
5
|
+
## v1.1.6 - 2026-07-27
|
|
6
|
+
|
|
7
|
+
Monorepo fixes found auditing how the platform scopes itself to a project. Every case was reproduced against a real directory tree before it was changed.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `be-readonly-fe` keeps the backend read-only in a monorepo. Its `readOnlyPaths` and `shellProtectedPaths` were anchored at the repository root, so `backend/**` and `apps/api/**` covered a backend sitting at the top of the tree and nothing else. Choosing the profile on a repository whose backend lives in `packages/api`, `apps/backend` or `services/payments` produced a profile that announced the backend was read-only while leaving every file in it writable — the one guarantee the profile exists to make. The backend directories under `apps/`, `packages/` and all of `services/*` are covered now.
|
|
12
|
+
|
|
13
|
+
They are enumerated rather than matched with `**/api/**`, which would have swept in `packages/web/src/api/` — the frontend's own HTTP client — and frozen the half of the repository the profile is meant to leave writable.
|
|
14
|
+
|
|
15
|
+
- The frontend verify command finds a frontend that is not at `frontend/` or `apps/web/`. It fell through to the repository root, where a workspace root has no `type-check` script, so verification reported success having run nothing. `apps/frontend`, `packages/web`, `packages/frontend` and `client` are looked at too.
|
|
16
|
+
|
|
17
|
+
- `apps/backend` is read as a backend. `apps/frontend` counted as a frontend marker while only `apps/api` and `apps/server` counted as backend ones, so a repository that named both halves symmetrically was detected as frontend-only and offered `web-frontend`. `client/` and `server/` at the root were not markers either.
|
|
18
|
+
|
|
19
|
+
- Frontend and backend are detected inside declared workspaces. Nothing read the `workspaces` field or `pnpm-workspace.yaml`, and nothing looked under `packages/`, so a pnpm monorepo fell through every marker to `generic`. Each declared package is now read through its own manifest and layout, which is what identifies `packages/storefront` and `packages/gateway` as the two halves of a fullstack repository. Only a trailing wildcard is expanded, absolute and `..` patterns are refused, and at most 64 packages are read.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Profile detection lives in `packages/piagent-core/extensions/project-shape.js`. `piagent_profile_options` and `scripts/init-project.sh` each carried their own copy of the rules and had drifted; the `apps/backend` gap above was present in both, and a fix to either would have left the other wrong. The file is covered by the capability lock, because steering which profile a project is offered is a way to choose what the guard ends up enforcing.
|
|
24
|
+
|
|
25
|
+
## v1.1.5 - 2026-07-27
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `piagent-update` moves a machine from one release to the next in one command. A release is three independently versioned things — the Pi host, the npm-global terminal helper, and the Pi package — and they have to move in that order, because `piagent-install` refuses to run against a host that does not match the version its own package.json pins and does not install one itself. Doing it by hand is three commands and one ordering rule that fails late when it is got wrong.
|
|
30
|
+
|
|
31
|
+
Every version the run needs is resolved from the registry before anything is installed, including the host version taken from the *target* helper's published metadata rather than the running one, so `--check` and `--dry-run` describe exactly what a real run would do. After the helper replaces itself the run confirms that the version on disk is the one it asked for, and stops before touching the Pi package if it is not. Running it from a git checkout is refused rather than replacing that checkout's commands with a published build.
|
|
32
|
+
|
|
33
|
+
`--check`, `--dry-run`, `--version X.Y.Z`, `--force`, `--project <path>`, `--no-host`, `--no-package`, and pass-through to `piagent-install` after `--`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- `docs/release-install-policy.md`, `docs/team-onboarding.md` and `docs/command-reference-vietnamese.md` described the manual update sequence without saying why its order is mandatory. `piagent-install` fails on a host mismatch; the auto-install of a matching host lives in `setup.sh` and does not apply to that flow.
|
|
38
|
+
|
|
5
39
|
## v1.1.4 - 2026-07-27
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ This repository intentionally excludes:
|
|
|
283
283
|
|
|
284
284
|
## Maturity
|
|
285
285
|
|
|
286
|
-
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.
|
|
286
|
+
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.6` or a reviewed commit.
|
|
287
287
|
|
|
288
288
|
Ready for:
|
|
289
289
|
|
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
"api/**",
|
|
33
33
|
"apps/api/**",
|
|
34
34
|
"apps/server/**",
|
|
35
|
-
"
|
|
35
|
+
"apps/backend/**",
|
|
36
|
+
"packages/api/**",
|
|
37
|
+
"packages/server/**",
|
|
38
|
+
"packages/backend/**",
|
|
39
|
+
"services/*/**",
|
|
36
40
|
"**/migrations/**"
|
|
37
41
|
],
|
|
38
42
|
"shellProtectedPaths": [
|
|
@@ -47,7 +51,11 @@
|
|
|
47
51
|
"api/**",
|
|
48
52
|
"apps/api/**",
|
|
49
53
|
"apps/server/**",
|
|
50
|
-
"
|
|
54
|
+
"apps/backend/**",
|
|
55
|
+
"packages/api/**",
|
|
56
|
+
"packages/server/**",
|
|
57
|
+
"packages/backend/**",
|
|
58
|
+
"services/*/**",
|
|
51
59
|
"**/migrations/**",
|
|
52
60
|
"**/node_modules/**",
|
|
53
61
|
"**/dist/**",
|
|
@@ -85,10 +93,10 @@
|
|
|
85
93
|
"test -s README.md"
|
|
86
94
|
],
|
|
87
95
|
"frontendSource": [
|
|
88
|
-
"if test -f frontend/package.json; then cd frontend
|
|
96
|
+
"if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend verify command configured; update .pi/piagent-profile.json'; exit 0; fi; npm run type-check --if-present && npm run lint --if-present && npm test --if-present"
|
|
89
97
|
],
|
|
90
98
|
"frontendRuntime": [
|
|
91
|
-
"if test -f frontend/package.json; then cd frontend
|
|
99
|
+
"if test -f frontend/package.json; then cd frontend; elif test -f apps/web/package.json; then cd apps/web; elif test -f apps/frontend/package.json; then cd apps/frontend; elif test -f packages/web/package.json; then cd packages/web; elif test -f packages/frontend/package.json; then cd packages/frontend; elif test -f client/package.json; then cd client; elif test -f package.json; then :; else echo 'No frontend runtime verify command configured; update .pi/piagent-profile.json'; exit 0; fi; npm run test:e2e --if-present"
|
|
92
100
|
]
|
|
93
101
|
},
|
|
94
102
|
"mcpCapabilities": [
|
|
@@ -419,10 +419,13 @@ Các lệnh này chạy ngoài Pi.
|
|
|
419
419
|
| Command | Dùng khi nào |
|
|
420
420
|
|---|---|
|
|
421
421
|
| `npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0` | Cài Pi CLI tương thích với release hiện tại. |
|
|
422
|
-
| `npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
423
|
-
| `pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
422
|
+
| `npm install -g --ignore-scripts @piagent/platform@1.1.6` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
|
|
423
|
+
| `pi install git:github.com/Vt-mmm/piagent@v1.1.6` | Cài pinned release khi cần reproducible team setup. |
|
|
424
424
|
| `pi install git:github.com/Vt-mmm/piagent` | Cài latest package platform cho máy cá nhân/sandbox. |
|
|
425
|
-
|
|
|
425
|
+
| `piagent-update --check` | Báo version hiện tại vs version sẽ lên cho cả ba thành phần; không cài gì. |
|
|
426
|
+
| `piagent-update` | Full update một lệnh: Pi host → npm-global helper → Pi package, đúng thứ tự release yêu cầu. Thêm `--project <path>` để chạy doctor sau. |
|
|
427
|
+
| `piagent-update --version X.Y.Z` | Update tới đúng một release thay vì latest; dùng cho rollout theo lô hoặc rollback. |
|
|
428
|
+
| Cài exact Pi host của release, rồi `npm install -g --ignore-scripts @piagent/platform@X.Y.Z` và `piagent-install --stable` | Sequence thủ công tương đương. Thứ tự bắt buộc: `piagent-install` fail nếu Pi host lệch bản pin và không tự cài host. Version pin của từng release nằm trong [release/install policy](release-install-policy.md). |
|
|
426
429
|
| Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá dependency risk của host cũ trước khi hạ version. |
|
|
427
430
|
| `piagent-install --stable --dry-run` | Preview Pi package matching với helper hiện tại; stable resolve tag → commit SHA. |
|
|
428
431
|
| `piagent-install --stable` | Cài Pi package matching với helper hiện tại bằng resolved commit SHA. |
|
|
@@ -14,9 +14,9 @@ Repo này phải chạy được cho nhiều project/domain khác nhau. Vì vậ
|
|
|
14
14
|
|
|
15
15
|
| Use case | Package source |
|
|
16
16
|
|---|---|
|
|
17
|
-
| Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.
|
|
17
|
+
| Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.6` |
|
|
18
18
|
| Personal/sandbox dev | `git:github.com/Vt-mmm/piagent` |
|
|
19
|
-
| Registry distribution | `npm:@piagent/platform@1.1.
|
|
19
|
+
| Registry distribution | `npm:@piagent/platform@1.1.6` |
|
|
20
20
|
| Enterprise npm (fork under another scope) | `npm:@your-scope/platform@x.y.z` |
|
|
21
21
|
| Local platform dev | `/path/to/piagent` |
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ Latest tiện cho máy cá nhân muốn nhận cập nhật nhanh. Pin tag/commi
|
|
|
26
26
|
|
|
27
27
|
| Channel | Command | Target | Policy |
|
|
28
28
|
|---|---|---|---|
|
|
29
|
-
| `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.
|
|
29
|
+
| `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.6` | Install the Pi package matching the helper. |
|
|
30
30
|
| `exact` | `bash scripts/install-global.sh --version vX.Y.Z --resolve-tag` | Requested tag resolved to commit SHA | Pi-package-only rollout, rollback, and incident recovery. |
|
|
31
31
|
| `dev` | `bash scripts/install-global.sh --dev` | Moving Git source | Personal/sandbox only; do not commit into project settings. |
|
|
32
32
|
| `local` | `bash scripts/install-global.sh --local` | Current checkout path | Platform development only. |
|
|
@@ -53,7 +53,7 @@ Root `package.json` có `pi` manifest trỏ tới:
|
|
|
53
53
|
Do đó team có thể:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
56
|
+
pi install git:github.com/Vt-mmm/piagent@v1.1.6
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Không cần biết internal folder `packages/piagent-core`. Source không pin chỉ dành cho personal/sandbox như bảng channel ở trên.
|
|
@@ -65,12 +65,12 @@ Team nên install global package một lần:
|
|
|
65
65
|
```bash
|
|
66
66
|
node --version # >= 22.19.0
|
|
67
67
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
68
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
68
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
69
69
|
piagent-install --stable --dry-run
|
|
70
70
|
piagent-install --stable
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
73
|
+
`pi install git:github.com/Vt-mmm/piagent@v1.1.6` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.1.6`.
|
|
74
74
|
|
|
75
75
|
Support matrix hiện tại: macOS Apple Silicon + Bash và Linux x64 + Bash đã được verify; macOS Intel + Bash và Linux ARM64 + Bash là supported target cần chạy smoke trước rollout; native Windows chưa phải target rollout team; WSL2 experimental. Chi tiết và version runtime nằm trong [release/install policy](release-install-policy.md).
|
|
76
76
|
|
|
@@ -103,7 +103,7 @@ Nếu muốn commit sẵn `.pi/piagent-profile.json` vào repo hoặc bootstrap
|
|
|
103
103
|
```bash
|
|
104
104
|
bash scripts/setup.sh /path/to/project \
|
|
105
105
|
--profile be-readonly-fe \
|
|
106
|
-
--package-source git:github.com/Vt-mmm/piagent@v1.1.
|
|
106
|
+
--package-source git:github.com/Vt-mmm/piagent@v1.1.6 \
|
|
107
107
|
--mcp-preset core \
|
|
108
108
|
--subagents-preset safe
|
|
109
109
|
```
|
|
@@ -18,12 +18,12 @@ Support matrix release hiện tại: macOS Apple Silicon + Bash và Linux x64 +
|
|
|
18
18
|
```bash
|
|
19
19
|
node --version # >= 22.19.0
|
|
20
20
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
21
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
21
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
22
22
|
piagent-install --stable --dry-run
|
|
23
23
|
piagent-install --stable
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.
|
|
26
|
+
Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.6`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
|
|
27
27
|
|
|
28
28
|
Nếu đang ở source checkout của platform, dùng helper theo channel để preview trước khi đổi:
|
|
29
29
|
|
|
@@ -99,7 +99,7 @@ install package once
|
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
101
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
102
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
102
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
103
103
|
piagent-install --stable --dry-run
|
|
104
104
|
piagent-install --stable
|
|
105
105
|
```
|
|
@@ -156,6 +156,18 @@ Trong Pi:
|
|
|
156
156
|
|
|
157
157
|
Chọn provider/account như OpenAI/Codex hoặc Anthropic/Claude. Credential nằm trong Pi user dir, không nằm trong repo và không được commit.
|
|
158
158
|
|
|
159
|
+
Browser login của OpenAI có hai chỗ trông như treo. Cả hai là lỗi Pi host, không phải platform, và còn nguyên ở `0.82.0` lẫn `0.82.1`.
|
|
160
|
+
|
|
161
|
+
Paste xong callback URL mà màn hình đứng im: **bấm Enter**. Ô nhập callback chỉ hiện hint `to cancel`, không hiện `to submit`, nên nó đang chờ submit chứ không đơ.
|
|
162
|
+
|
|
163
|
+
Đã Enter rồi mà terminal khoá luôn, Esc không thoát được: Pi lưu credential xong mới refresh model catalog từ `pi.dev`, và request đó không có timeout. Firewall hoặc proxy giữ connection im lặng thì `/login` chờ mãi. Credential thực tế đã lưu rồi, nên mở tab mới vẫn dùng được. Login bằng đường này để tránh hẳn:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pi --offline
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Rồi `/login` → browser → paste → Enter. `--offline` chỉ chặn bước refresh catalog gây treo, OAuth vẫn chạy. Xong thì thoát và mở `pi` bình thường.
|
|
170
|
+
|
|
159
171
|
### Chọn model và thinking
|
|
160
172
|
|
|
161
173
|
Trong Pi:
|
|
@@ -269,7 +281,7 @@ Không phải daily default. Dùng khi muốn tạo sẵn `.pi` files cho projec
|
|
|
269
281
|
bash /path/to/piagent/scripts/setup.sh /path/to/project \
|
|
270
282
|
--project-only \
|
|
271
283
|
--profile auto \
|
|
272
|
-
--package-source git:github.com/Vt-mmm/piagent@v1.1.
|
|
284
|
+
--package-source git:github.com/Vt-mmm/piagent@v1.1.6 \
|
|
273
285
|
--mcp-preset core \
|
|
274
286
|
--subagents-preset safe
|
|
275
287
|
```
|
|
@@ -842,8 +854,8 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
|
|
|
842
854
|
|
|
843
855
|
| Command | Dùng để |
|
|
844
856
|
|---|---|
|
|
845
|
-
| `npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
846
|
-
| `pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
857
|
+
| `npm install -g --ignore-scripts @piagent/platform@1.1.6` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
|
|
858
|
+
| `pi install git:github.com/Vt-mmm/piagent@v1.1.6` | Install pinned release cho reproducible team setup. |
|
|
847
859
|
| `pi install git:github.com/Vt-mmm/piagent` | Install latest platform package cho máy cá nhân/sandbox. |
|
|
848
860
|
| Cài exact Pi host của release, rồi `npm install -g --ignore-scripts @piagent/platform@X.Y.Z` và `piagent-install --stable` | Full update: đồng bộ host, terminal helper và Pi package. Mỗi release pin một Pi host chính xác; lấy đúng version của release đang cài trong [release/install policy](release-install-policy.md). |
|
|
849
861
|
| Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá lại dependency findings của host cũ trước khi hạ version. |
|
|
@@ -940,7 +952,7 @@ Mở lại Pi session sau khi install.
|
|
|
940
952
|
Cài lại terminal helper đúng release rồi kiểm tra `PATH`:
|
|
941
953
|
|
|
942
954
|
```bash
|
|
943
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
955
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
944
956
|
command -v piagent-install
|
|
945
957
|
```
|
|
946
958
|
|
package/docs/project-adapters.md
CHANGED
|
@@ -68,6 +68,22 @@ Path policy fields have distinct meanings:
|
|
|
68
68
|
- `readOnlyPaths`: allow `read`/`grep`/`find`/`ls`, but block `write`/`edit` and shell access.
|
|
69
69
|
- `shellProtectedPaths`: block shell access only. Do not rely on this field to block write/edit; `profile-doctor` and `team-doctor` warn when a path is present only here.
|
|
70
70
|
|
|
71
|
+
Glob trong các field này neo từ project root, nên `backend/**` chỉ khớp backend nằm ở root. Với monorepo, phải liệt kê đúng vị trí thật (`packages/api/**`, `services/*/**`). Không dùng `**/api/**`: nó khớp luôn `packages/web/src/api/` — thư mục HTTP client của frontend — và biến chính phần được phép sửa thành read-only.
|
|
72
|
+
|
|
73
|
+
## Monorepo và workspace
|
|
74
|
+
|
|
75
|
+
Một project = một thư mục mở `pi`. Guard đọc profile tại `<cwd>/.pi/piagent-profile.json` và không đi ngược lên tìm, nên mở `pi` ở thư mục cha chứa nhiều project sẽ bỏ qua profile của từng project con và chạy unprofiled.
|
|
76
|
+
|
|
77
|
+
Detect FE/BE đọc theo thứ tự:
|
|
78
|
+
|
|
79
|
+
1. Tên thư mục ở root: `frontend`, `web`, `client`, `ui` cho FE; `backend`, `server`, `api` cho BE.
|
|
80
|
+
2. Cùng các tên đó ở một cấp dưới `apps/`, `packages/`, `services/`.
|
|
81
|
+
3. Package khai báo trong `workspaces` (package.json, cả dạng mảng lẫn `{ "packages": [...] }`) hoặc `pnpm-workspace.yaml` — mỗi package đọc dependency và config của chính nó, nên `packages/storefront` + `packages/gateway` vẫn ra `fullstack` dù tên không gợi ý gì.
|
|
82
|
+
|
|
83
|
+
Chỉ wildcard ở segment cuối được mở rộng (`packages/*`, `apps/**`); pattern tuyệt đối, pattern có `..`, và pattern loại trừ `!` bị bỏ qua. Số package đọc tối đa 64.
|
|
84
|
+
|
|
85
|
+
Rule nằm ở `packages/piagent-core/extensions/project-shape.js`. Cả `piagent_profile_options` lẫn `scripts/init-project.sh` cùng gọi file này; trước đây mỗi bên giữ một bản riêng và đã lệch nhau.
|
|
86
|
+
|
|
71
87
|
## Built-in adapters
|
|
72
88
|
|
|
73
89
|
| Profile | Dùng cho | Verify mặc định |
|
|
@@ -84,6 +100,11 @@ Path policy fields have distinct meanings:
|
|
|
84
100
|
| `mobile` | React Native/Flutter | npm test / flutter test nếu tool có |
|
|
85
101
|
| `docs` | Docs portal/manual | markdown diff check + test nếu project có |
|
|
86
102
|
|
|
103
|
+
`be-readonly-fe` giữ read-only cho backend ở root (`backend`, `server`, `api`), ở một cấp dưới `apps/` và `packages/` (`api`, `server`, `backend`), toàn bộ `services/*`, và mọi `**/migrations/**`. Profile built-in là điểm khởi đầu, không phải danh sách đầy đủ — sửa `readOnlyPaths` **và** `shellProtectedPaths` trong `.pi/piagent-profile.json` cho đúng repo:
|
|
104
|
+
|
|
105
|
+
- backend đặt tên khác (`apps/gateway`, `packages/core-service`): thêm path vào cả hai field.
|
|
106
|
+
- frontend nằm trong `services/` (ví dụ `services/web`): `services/*/**` sẽ khoá nhầm nó. Thay pattern đó bằng danh sách service backend cụ thể. Triệu chứng là một write bị chặn kèm lý do trỏ tới `readOnlyPaths`, không phải lỗi im lặng.
|
|
107
|
+
|
|
87
108
|
## Runtime profile selection
|
|
88
109
|
|
|
89
110
|
Default UX không bắt buộc chạy bash để set profile. Sau global install, vào project chạy:
|
|
@@ -47,6 +47,8 @@ pi install /path/to/piagent
|
|
|
47
47
|
- `1.1.2`: đóng TOCTOU của document reader, hash evidence tính trên text đã redact, publish chờ đủ verify matrix.
|
|
48
48
|
- `1.1.3`: command mang secret không còn được dùng làm verify evidence, document reader từ chối FIFO thay vì treo, redact secret ngắn, redirect phải về đúng trang được kiểm.
|
|
49
49
|
- `1.1.4`: nhận diện secret dạng CLI option (`--token`, `--password`, `--api-key`) và Authorization header ở mọi vị trí argument.
|
|
50
|
+
- `1.1.5`: thêm `piagent-update` — một lệnh update cả Pi host, terminal helper và Pi package đúng thứ tự.
|
|
51
|
+
- `1.1.6`: `be-readonly-fe` giữ được backend read-only trong monorepo, detect FE/BE đọc cả workspace, và rule detect gộp về một chỗ.
|
|
50
52
|
|
|
51
53
|
## Không publish
|
|
52
54
|
|
|
@@ -10,7 +10,7 @@ This file is the canonical install, update, rollback, and release checklist. Oth
|
|
|
10
10
|
|
|
11
11
|
All supported environments require Node.js `>=22.19.0` and Pi Coding Agent `0.82.0`. The Pi host is installed as a Node CLI; Pi Agent Platform still defines its own release matrix because the terminal helpers and shell policy rely on Bash/POSIX behavior.
|
|
12
12
|
|
|
13
|
-
| Surface | Status for v1.1.
|
|
13
|
+
| Surface | Status for v1.1.6 | Rollout guidance |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| macOS Apple Silicon (`darwin/arm64`) + Bash | Verified for this release. | Safe default for team rollout after normal project smoke tests. |
|
|
16
16
|
| Linux x64 + Bash | Verified in GitHub Actions. | Safe default for CI/server usage after normal project smoke tests. |
|
|
@@ -33,7 +33,7 @@ These three components are versioned independently. A full install, update, or r
|
|
|
33
33
|
|
|
34
34
|
| Channel | Source shape | Mutability | Use when |
|
|
35
35
|
|---|---|---:|---|
|
|
36
|
-
| `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.
|
|
36
|
+
| `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.6` | Fixed after resolution | Default for team rollout. |
|
|
37
37
|
| `exact` | Tag, reviewed commit, or a tag resolved with `--resolve-tag` | Fixed when using a commit SHA | Pi-package-only roll forward, rollback, or reproduction. |
|
|
38
38
|
| `dev` | `git:github.com/Vt-mmm/piagent` | Moving | Personal machine or sandbox only. |
|
|
39
39
|
| `local` | `/path/to/piagent` | Local workspace | Platform development and dry-run validation. |
|
|
@@ -53,7 +53,7 @@ Use this flow when the team needs both terminal commands and the Pi package:
|
|
|
53
53
|
```bash
|
|
54
54
|
node --version # >= 22.19.0
|
|
55
55
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
56
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
56
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
57
57
|
piagent-install --stable --dry-run
|
|
58
58
|
piagent-install --stable
|
|
59
59
|
```
|
|
@@ -61,8 +61,8 @@ piagent-install --stable
|
|
|
61
61
|
The stable preview and apply output includes:
|
|
62
62
|
|
|
63
63
|
```text
|
|
64
|
-
currentRelease: v1.1.
|
|
65
|
-
tag: v1.1.
|
|
64
|
+
currentRelease: v1.1.6 (helper package version)
|
|
65
|
+
tag: v1.1.6
|
|
66
66
|
resolvedCommit: <40-char-sha>
|
|
67
67
|
source: git:github.com/Vt-mmm/piagent@<40-char-sha>
|
|
68
68
|
```
|
|
@@ -79,14 +79,26 @@ bash scripts/install-global.sh --stable
|
|
|
79
79
|
Use this only when terminal commands are not needed:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
82
|
+
pi install git:github.com/Vt-mmm/piagent@v1.1.6
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Direct `pi install` does not create `piagent-*` commands on `PATH`.
|
|
86
86
|
|
|
87
87
|
## Full platform update
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
`piagent-update` does the whole sequence in one command:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
piagent-update --check
|
|
93
|
+
piagent-update --dry-run
|
|
94
|
+
piagent-update --project /path/to/project
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
It resolves the target helper version and, from that version's published metadata, the exact Pi host that version pins — both before installing anything, so `--check` and `--dry-run` describe a real run. It then installs the host, replaces the helper, verifies that the version now on disk is the one it asked for, and only then runs the new helper's installer for the Pi package. `--version X.Y.Z` targets an exact release instead of latest, `--no-host` and `--no-package` narrow the run, and anything after `--` is passed to `piagent-install`.
|
|
98
|
+
|
|
99
|
+
Running it from a git checkout is refused rather than replacing that checkout's commands with a published build; update a checkout with `git pull` and `install-global.sh --local`.
|
|
100
|
+
|
|
101
|
+
The same sequence by hand, which must keep this order because `piagent-install` fails against a host that does not match the version it pins and does not install one itself:
|
|
90
102
|
|
|
91
103
|
```bash
|
|
92
104
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
@@ -21,7 +21,7 @@ This document covers the packaged guard, project profiles, capability locks, ins
|
|
|
21
21
|
- The operator, operating-system account, Pi host, and explicitly trusted project repository are trusted to execute code.
|
|
22
22
|
- Project-local Pi resources are loaded only after Pi project trust or an explicit operator override.
|
|
23
23
|
- A human remains available to approve destructive and external-provider actions.
|
|
24
|
-
- Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.
|
|
24
|
+
- Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.6, and WSL2 is experimental/unverified.
|
|
25
25
|
- Registry, GitHub, model-provider, MCP-provider, and Vercel controls are external dependencies. Their account security and platform guarantees are not replaced by this repository.
|
|
26
26
|
|
|
27
27
|
## Threat actors and failure modes
|
package/docs/team-onboarding.md
CHANGED
|
@@ -9,7 +9,7 @@ Một thành viên mới không cần biết local path của maintainer. Luồn
|
|
|
9
9
|
```bash
|
|
10
10
|
node --version # >= 22.19.0
|
|
11
11
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
12
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
12
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
13
13
|
piagent-install --stable
|
|
14
14
|
cd /path/to/project
|
|
15
15
|
pi
|
|
@@ -42,7 +42,7 @@ Mặc định team dùng stable helper:
|
|
|
42
42
|
```bash
|
|
43
43
|
node --version # >= 22.19.0
|
|
44
44
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
45
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
45
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.6
|
|
46
46
|
piagent-install --stable --dry-run
|
|
47
47
|
piagent-install --stable
|
|
48
48
|
```
|
|
@@ -63,7 +63,16 @@ pi install git:github.com/Vt-mmm/piagent
|
|
|
63
63
|
|
|
64
64
|
## Update và rollback cho team
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Một lệnh, đúng thứ tự, cho cả ba thành phần:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
piagent-update --check
|
|
70
|
+
piagent-update --project /path/to/project
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.1.6`.
|
|
74
|
+
|
|
75
|
+
Sequence thủ công vẫn dùng được, và bắt buộc theo thứ tự này — `piagent-install` **fail** nếu Pi host lệch bản pin, nó không tự nâng host:
|
|
67
76
|
|
|
68
77
|
```bash
|
|
69
78
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
@@ -108,6 +117,8 @@ pi
|
|
|
108
117
|
|
|
109
118
|
Credential nằm trong user Pi dir, không commit.
|
|
110
119
|
|
|
120
|
+
Nếu browser login của OpenAI trông như treo, xem [login provider](operator-manual-vietnamese.md#login-provider). Tóm tắt: paste callback URL xong phải bấm **Enter** (màn hình không nhắc), và nếu sau Enter terminal khoá luôn thì login bằng `pi --offline` một lần. Cả hai là lỗi Pi host `0.82.0`/`0.82.1`, không phải platform.
|
|
121
|
+
|
|
111
122
|
## Bước 3 — chọn model và onboard project
|
|
112
123
|
|
|
113
124
|
Sau login, chọn provider/model intended cho project understanding bằng native Pi selector. OpenAI Codex và Claude/Anthropic đều là supported option.
|
package/docs-site/index.html
CHANGED
|
@@ -798,7 +798,7 @@
|
|
|
798
798
|
<span>Pi Agent Platform</span>
|
|
799
799
|
</a>
|
|
800
800
|
<div class="top-actions">
|
|
801
|
-
<span class="pill"><span class="pill-dot"></span>v1.1.
|
|
801
|
+
<span class="pill"><span class="pill-dot"></span>v1.1.6 docs</span>
|
|
802
802
|
<a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
|
|
803
803
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
804
804
|
<path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
@@ -934,7 +934,7 @@
|
|
|
934
934
|
<article class="step">
|
|
935
935
|
<div>
|
|
936
936
|
<h3>Cài — hai lệnh</h3>
|
|
937
|
-
<p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.
|
|
937
|
+
<p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.6</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
|
|
938
938
|
<div class="table-wrap compact-table">
|
|
939
939
|
<table>
|
|
940
940
|
<thead>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piagent/platform",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"bin": {
|
|
48
48
|
"piagent-setup": "scripts/piagent-cli.mjs",
|
|
49
49
|
"piagent-install": "scripts/piagent-cli.mjs",
|
|
50
|
+
"piagent-update": "scripts/piagent-cli.mjs",
|
|
50
51
|
"piagent-uninstall": "scripts/piagent-cli.mjs",
|
|
51
52
|
"piagent-init": "scripts/piagent-cli.mjs",
|
|
52
53
|
"piagent-doctor": "scripts/piagent-cli.mjs",
|
|
@@ -795,7 +795,11 @@ function buildCoreIntegrity(root) {
|
|
|
795
795
|
"packages/piagent-core/extensions/guard-shell-analysis.ts",
|
|
796
796
|
"packages/piagent-core/extensions/policy-core.js",
|
|
797
797
|
"packages/piagent-core/extensions/redaction-core.js",
|
|
798
|
-
"packages/piagent-core/extensions/runtime-evidence.js"
|
|
798
|
+
"packages/piagent-core/extensions/runtime-evidence.js",
|
|
799
|
+
// project-shape.js picks the profile a project is offered, and the profile is
|
|
800
|
+
// the document that carries protectedPaths and readOnlyPaths. Steering the
|
|
801
|
+
// recommendation is a way to choose what the guard will end up enforcing.
|
|
802
|
+
"packages/piagent-core/extensions/project-shape.js"
|
|
799
803
|
];
|
|
800
804
|
return files.map((relativePath) => {
|
|
801
805
|
const resolved = resolveRegularFile(root, relativePath, MAX_ARTIFACT_BYTES);
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
redactForStorage,
|
|
40
40
|
redactSensitiveText
|
|
41
41
|
} from "./redaction-core.js";
|
|
42
|
+
import { detectProfileName } from "./project-shape.js";
|
|
42
43
|
import {
|
|
43
44
|
resolveCapabilityProfileDocument,
|
|
44
45
|
verifyCapabilityLock,
|
|
@@ -2636,75 +2637,6 @@ function readAdapterProfiles(extensionDir: string): Array<{ name: string; profil
|
|
|
2636
2637
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2637
2638
|
}
|
|
2638
2639
|
|
|
2639
|
-
function packageHasAny(cwd: string, pattern: RegExp): boolean {
|
|
2640
|
-
try {
|
|
2641
|
-
return pattern.test(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
|
|
2642
|
-
} catch {
|
|
2643
|
-
return false;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
function detectProfileName(cwd: string, intent?: string): { name: string; reason: string } {
|
|
2648
|
-
const normalizedIntent = intent?.trim().toLowerCase();
|
|
2649
|
-
if (normalizedIntent === "be-readonly-fe") return { name: "be-readonly-fe", reason: "User intent says backend should be read-only while frontend is the write target." };
|
|
2650
|
-
if (normalizedIntent === "frontend-only") return { name: "web-frontend", reason: "User intent says frontend-only." };
|
|
2651
|
-
if (normalizedIntent === "backend-only") return { name: "backend-api", reason: "User intent says backend-only." };
|
|
2652
|
-
if (normalizedIntent === "docs") return { name: "docs", reason: "User intent says docs/docs-only." };
|
|
2653
|
-
|
|
2654
|
-
const hasPackage = fs.existsSync(path.join(cwd, "package.json"));
|
|
2655
|
-
let frontend = false;
|
|
2656
|
-
let backend = false;
|
|
2657
|
-
let data = false;
|
|
2658
|
-
let mobile = false;
|
|
2659
|
-
let infra = false;
|
|
2660
|
-
let docs = false;
|
|
2661
|
-
|
|
2662
|
-
if (fs.existsSync(path.join(cwd, "pubspec.yaml")) || (fs.existsSync(path.join(cwd, "android")) && fs.existsSync(path.join(cwd, "ios")))) mobile = true;
|
|
2663
|
-
if (fs.existsSync(path.join(cwd, "dbt_project.yml")) || fs.existsSync(path.join(cwd, "dvc.yaml")) || fs.existsSync(path.join(cwd, "notebooks")) || fs.existsSync(path.join(cwd, "data"))) data = true;
|
|
2664
|
-
if (fs.existsSync(path.join(cwd, "Dockerfile")) || fs.existsSync(path.join(cwd, "docker-compose.yml")) || fs.existsSync(path.join(cwd, "compose.yml")) || fs.existsSync(path.join(cwd, "compose.yaml")) || fs.existsSync(path.join(cwd, "terraform")) || fs.existsSync(path.join(cwd, "infra")) || fs.existsSync(path.join(cwd, "k8s")) || fs.existsSync(path.join(cwd, "helm"))) infra = true;
|
|
2665
|
-
if (fs.existsSync(path.join(cwd, "docs")) || fs.existsSync(path.join(cwd, "mkdocs.yml")) || fs.existsSync(path.join(cwd, "mint.json")) || fs.existsSync(path.join(cwd, "docusaurus.config.js"))) docs = true;
|
|
2666
|
-
|
|
2667
|
-
if (hasPackage) {
|
|
2668
|
-
if (fs.existsSync(path.join(cwd, "frontend")) || fs.existsSync(path.join(cwd, "apps/web")) || fs.existsSync(path.join(cwd, "apps/frontend"))) frontend = true;
|
|
2669
|
-
if (fs.existsSync(path.join(cwd, "backend")) || fs.existsSync(path.join(cwd, "apps/api")) || fs.existsSync(path.join(cwd, "apps/server"))) backend = true;
|
|
2670
|
-
if (packageHasAny(cwd, /"(next|react|vite|vue|svelte|astro|@angular\/core|remix)"/i)
|
|
2671
|
-
|| fs.existsSync(path.join(cwd, "next.config.js"))
|
|
2672
|
-
|| fs.existsSync(path.join(cwd, "next.config.mjs"))
|
|
2673
|
-
|| fs.existsSync(path.join(cwd, "next.config.ts"))
|
|
2674
|
-
|| fs.existsSync(path.join(cwd, "vite.config.js"))
|
|
2675
|
-
|| fs.existsSync(path.join(cwd, "vite.config.ts"))
|
|
2676
|
-
|| fs.existsSync(path.join(cwd, "src/app"))
|
|
2677
|
-
|| fs.existsSync(path.join(cwd, "pages"))
|
|
2678
|
-
|| fs.existsSync(path.join(cwd, "public"))) frontend = true;
|
|
2679
|
-
if (packageHasAny(cwd, /"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"/i)
|
|
2680
|
-
|| fs.existsSync(path.join(cwd, "nest-cli.json"))
|
|
2681
|
-
|| fs.existsSync(path.join(cwd, "prisma"))
|
|
2682
|
-
|| fs.existsSync(path.join(cwd, "src/server"))
|
|
2683
|
-
|| fs.existsSync(path.join(cwd, "src/api"))) backend = true;
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
if (fs.existsSync(path.join(cwd, "pom.xml")) || fs.existsSync(path.join(cwd, "build.gradle")) || fs.existsSync(path.join(cwd, "build.gradle.kts")) || fs.existsSync(path.join(cwd, "src/main/java")) || fs.existsSync(path.join(cwd, "src/main/kotlin"))) backend = true;
|
|
2687
|
-
|
|
2688
|
-
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) {
|
|
2689
|
-
try {
|
|
2690
|
-
if (/(fastapi|flask|django|litestar|starlite)/i.test(fs.readFileSync(path.join(cwd, "pyproject.toml"), "utf8"))) backend = true;
|
|
2691
|
-
} catch {
|
|
2692
|
-
// ignore unreadable pyproject for recommendation
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
if (mobile) return { name: "mobile", reason: "Mobile markers found." };
|
|
2697
|
-
if (frontend && backend) return { name: "fullstack", reason: "Frontend and backend markers both found. Pick be-readonly-fe instead if backend must be read-only." };
|
|
2698
|
-
if (frontend) return { name: "web-frontend", reason: "Frontend framework markers found." };
|
|
2699
|
-
if (backend) return { name: "backend-api", reason: "Backend/API markers found." };
|
|
2700
|
-
if (data) return { name: "data", reason: "Data/ETL markers found." };
|
|
2701
|
-
if (fs.existsSync(path.join(cwd, "pyproject.toml"))) return { name: "python", reason: "Python pyproject.toml found." };
|
|
2702
|
-
if (hasPackage && fs.existsSync(path.join(cwd, "tsconfig.json"))) return { name: "node-typescript", reason: "Node TypeScript markers found." };
|
|
2703
|
-
if (infra) return { name: "devops", reason: "Infrastructure markers found." };
|
|
2704
|
-
if (docs) return { name: "docs", reason: "Documentation markers found." };
|
|
2705
|
-
return { name: "generic", reason: "No stronger project markers found." };
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
2640
|
function profileDescription(name: string): string {
|
|
2709
2641
|
const descriptions: Record<string, string> = {
|
|
2710
2642
|
generic: "Safe baseline for unknown repos.",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// The shape of a repository, in the only terms the profiles care about: is there
|
|
5
|
+
// a frontend, is there a backend, and what else is sitting at the root. Both the
|
|
6
|
+
// runtime recommendation (`piagent_profile_options`) and the shell initialiser
|
|
7
|
+
// (`scripts/init-project.sh`) read it from here. They used to carry separate
|
|
8
|
+
// copies of the same rules, which is how `apps/backend` ended up being invisible
|
|
9
|
+
// to one of them for several releases.
|
|
10
|
+
|
|
11
|
+
// Directories that name a side of the stack outright. These are matched at the
|
|
12
|
+
// repository root and one level under each workspace root, never at arbitrary
|
|
13
|
+
// depth: `src/api` inside a frontend package is an HTTP client, not a backend.
|
|
14
|
+
const FRONTEND_DIR_NAMES = ["frontend", "web", "client", "ui"];
|
|
15
|
+
const BACKEND_DIR_NAMES = ["backend", "server", "api"];
|
|
16
|
+
const WORKSPACE_ROOTS = ["apps", "packages", "services"];
|
|
17
|
+
|
|
18
|
+
const FRONTEND_DEPENDENCIES = /"(next|react|vite|vue|svelte|astro|@angular\/core|remix)"/i;
|
|
19
|
+
const BACKEND_DEPENDENCIES = /"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"/i;
|
|
20
|
+
const PYTHON_BACKEND_FRAMEWORKS = /(fastapi|flask|django|litestar|starlite)/i;
|
|
21
|
+
|
|
22
|
+
const FRONTEND_MARKER_FILES = [
|
|
23
|
+
"next.config.js",
|
|
24
|
+
"next.config.mjs",
|
|
25
|
+
"next.config.ts",
|
|
26
|
+
"vite.config.js",
|
|
27
|
+
"vite.config.ts",
|
|
28
|
+
"src/app",
|
|
29
|
+
"pages",
|
|
30
|
+
"public"
|
|
31
|
+
];
|
|
32
|
+
const NODE_BACKEND_MARKER_FILES = ["nest-cli.json", "prisma", "src/server", "src/api"];
|
|
33
|
+
// Markers that identify a backend on their own, with no manifest to lean on.
|
|
34
|
+
const NATIVE_BACKEND_MARKER_FILES = [
|
|
35
|
+
"pom.xml",
|
|
36
|
+
"build.gradle",
|
|
37
|
+
"build.gradle.kts",
|
|
38
|
+
"src/main/java",
|
|
39
|
+
"src/main/kotlin"
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// A workspace file can declare any number of packages. Reading a bounded number
|
|
43
|
+
// of them keeps a repository with hundreds of packages from turning profile
|
|
44
|
+
// recommendation into a directory walk.
|
|
45
|
+
const MAX_WORKSPACE_PACKAGES = 64;
|
|
46
|
+
|
|
47
|
+
function exists(dir, relative) {
|
|
48
|
+
try {
|
|
49
|
+
return fs.existsSync(path.join(dir, relative));
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The stack-naming markers are directories. A file called `api` says nothing
|
|
56
|
+
// about a repository having a backend in it.
|
|
57
|
+
function isDirectory(dir, relative) {
|
|
58
|
+
try {
|
|
59
|
+
return fs.statSync(path.join(dir, relative)).isDirectory();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readFile(dir, relative) {
|
|
66
|
+
try {
|
|
67
|
+
return fs.readFileSync(path.join(dir, relative), "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readJson(dir, relative) {
|
|
74
|
+
const text = readFile(dir, relative);
|
|
75
|
+
if (text === undefined) return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stripQuotes(value) {
|
|
84
|
+
return value.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// npm and yarn put the globs in package.json, pnpm puts them in its own file.
|
|
88
|
+
// Both are read: a repository is free to carry either, and some carry both.
|
|
89
|
+
function readWorkspacePatterns(cwd) {
|
|
90
|
+
const patterns = [];
|
|
91
|
+
|
|
92
|
+
const declared = readJson(cwd, "package.json")?.workspaces;
|
|
93
|
+
if (Array.isArray(declared)) {
|
|
94
|
+
patterns.push(...declared);
|
|
95
|
+
} else if (Array.isArray(declared?.packages)) {
|
|
96
|
+
patterns.push(...declared.packages);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
patterns.push(...readPnpmWorkspacePatterns(cwd));
|
|
100
|
+
return patterns.filter((pattern) => typeof pattern === "string");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A three-line YAML reader rather than a YAML dependency: this package ships
|
|
104
|
+
// with no runtime dependencies, and the shape being read is a single list of
|
|
105
|
+
// strings under one key.
|
|
106
|
+
function readPnpmWorkspacePatterns(cwd) {
|
|
107
|
+
const text = readFile(cwd, "pnpm-workspace.yaml") ?? readFile(cwd, "pnpm-workspace.yml");
|
|
108
|
+
if (text === undefined) return [];
|
|
109
|
+
|
|
110
|
+
const patterns = [];
|
|
111
|
+
let insidePackages = false;
|
|
112
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
113
|
+
const line = rawLine.replace(/#.*$/, "");
|
|
114
|
+
if (/^packages\s*:/.test(line)) {
|
|
115
|
+
insidePackages = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!insidePackages) continue;
|
|
119
|
+
|
|
120
|
+
const item = line.match(/^\s+-\s*(.+?)\s*$/);
|
|
121
|
+
if (item) {
|
|
122
|
+
patterns.push(stripQuotes(item[1]));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// A blank line stays inside the list; anything else starts the next key.
|
|
126
|
+
if (line.trim().length > 0) break;
|
|
127
|
+
}
|
|
128
|
+
return patterns;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function expandWorkspacePattern(cwd, pattern) {
|
|
132
|
+
const normalized = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
133
|
+
// `!` excludes rather than includes, and neither an absolute path nor one that
|
|
134
|
+
// climbs out of the repository is a workspace of this repository.
|
|
135
|
+
if (!normalized || normalized.startsWith("!") || normalized.startsWith("/")) return [];
|
|
136
|
+
|
|
137
|
+
const segments = normalized.split("/").filter((segment) => segment && segment !== ".");
|
|
138
|
+
if (segments.length === 0 || segments.includes("..")) return [];
|
|
139
|
+
|
|
140
|
+
const wildcardAt = segments.findIndex((segment) => segment.includes("*"));
|
|
141
|
+
if (wildcardAt === -1) return [segments.join("/")];
|
|
142
|
+
// Only a trailing wildcard is expanded. `packages/*` and `apps/**` are what
|
|
143
|
+
// workspace files actually declare; anything deeper is left to the directory
|
|
144
|
+
// and dependency markers instead of growing a glob engine in here.
|
|
145
|
+
if (wildcardAt !== segments.length - 1) return [];
|
|
146
|
+
|
|
147
|
+
const base = segments.slice(0, wildcardAt).join("/");
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = fs.readdirSync(base ? path.join(cwd, base) : cwd, { withFileTypes: true });
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return entries
|
|
155
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
156
|
+
.map((entry) => (base ? `${base}/${entry.name}` : entry.name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function readWorkspacePackageDirs(cwd) {
|
|
160
|
+
const found = [];
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const pattern of readWorkspacePatterns(cwd)) {
|
|
163
|
+
for (const relative of expandWorkspacePattern(cwd, pattern)) {
|
|
164
|
+
if (seen.has(relative)) continue;
|
|
165
|
+
if (!exists(cwd, relative)) continue;
|
|
166
|
+
seen.add(relative);
|
|
167
|
+
found.push(relative);
|
|
168
|
+
if (found.length >= MAX_WORKSPACE_PACKAGES) return found;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return found;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// What one directory says about itself, from its manifest and its own layout.
|
|
175
|
+
// Applied to the repository root and to every declared workspace package, so a
|
|
176
|
+
// pnpm monorepo whose packages are named `web` and `api` is read the same way a
|
|
177
|
+
// single-package repository is.
|
|
178
|
+
function directorySignals(dir) {
|
|
179
|
+
const manifest = readFile(dir, "package.json");
|
|
180
|
+
let frontend = false;
|
|
181
|
+
let backend = false;
|
|
182
|
+
|
|
183
|
+
if (manifest !== undefined) {
|
|
184
|
+
frontend = FRONTEND_DEPENDENCIES.test(manifest);
|
|
185
|
+
backend = BACKEND_DEPENDENCIES.test(manifest);
|
|
186
|
+
// Node layout markers only mean something next to a manifest. `public/` and
|
|
187
|
+
// `pages/` are ordinary directory names in repositories that have nothing to
|
|
188
|
+
// do with a web framework.
|
|
189
|
+
if (!frontend) frontend = FRONTEND_MARKER_FILES.some((marker) => exists(dir, marker));
|
|
190
|
+
if (!backend) backend = NODE_BACKEND_MARKER_FILES.some((marker) => exists(dir, marker));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!backend) backend = NATIVE_BACKEND_MARKER_FILES.some((marker) => exists(dir, marker));
|
|
194
|
+
|
|
195
|
+
const pyproject = readFile(dir, "pyproject.toml");
|
|
196
|
+
if (pyproject !== undefined && PYTHON_BACKEND_FRAMEWORKS.test(pyproject)) backend = true;
|
|
197
|
+
|
|
198
|
+
return { frontend, backend };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function shapeFromDirName(name) {
|
|
202
|
+
const normalized = name.toLowerCase();
|
|
203
|
+
if (FRONTEND_DIR_NAMES.includes(normalized)) return "frontend";
|
|
204
|
+
if (BACKEND_DIR_NAMES.includes(normalized)) return "backend";
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function detectProjectShape(cwd) {
|
|
209
|
+
const hasPackage = exists(cwd, "package.json");
|
|
210
|
+
const shape = {
|
|
211
|
+
hasPackage,
|
|
212
|
+
frontend: false,
|
|
213
|
+
backend: false,
|
|
214
|
+
data: false,
|
|
215
|
+
mobile: false,
|
|
216
|
+
infra: false,
|
|
217
|
+
docs: false,
|
|
218
|
+
workspacePackages: []
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (exists(cwd, "pubspec.yaml") || (exists(cwd, "android") && exists(cwd, "ios"))) shape.mobile = true;
|
|
222
|
+
if (exists(cwd, "dbt_project.yml") || exists(cwd, "dvc.yaml") || exists(cwd, "notebooks") || exists(cwd, "data")) {
|
|
223
|
+
shape.data = true;
|
|
224
|
+
}
|
|
225
|
+
if (exists(cwd, "Dockerfile") || exists(cwd, "docker-compose.yml") || exists(cwd, "compose.yml")
|
|
226
|
+
|| exists(cwd, "compose.yaml") || exists(cwd, "terraform") || exists(cwd, "infra")
|
|
227
|
+
|| exists(cwd, "k8s") || exists(cwd, "helm")) {
|
|
228
|
+
shape.infra = true;
|
|
229
|
+
}
|
|
230
|
+
if (exists(cwd, "docs") || exists(cwd, "mkdocs.yml") || exists(cwd, "mint.json") || exists(cwd, "docusaurus.config.js")) {
|
|
231
|
+
shape.docs = true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Directory names are read whether or not the root carries a package.json.
|
|
235
|
+
// A Django and React repository has `frontend/` and `backend/` and no root
|
|
236
|
+
// manifest at all; gating this on package.json used to hide both sides of it.
|
|
237
|
+
for (const name of [...FRONTEND_DIR_NAMES, ...BACKEND_DIR_NAMES]) {
|
|
238
|
+
const side = shapeFromDirName(name);
|
|
239
|
+
if (isDirectory(cwd, name)) shape[side] = true;
|
|
240
|
+
for (const root of WORKSPACE_ROOTS) {
|
|
241
|
+
if (isDirectory(cwd, `${root}/${name}`)) shape[side] = true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const rootSignals = directorySignals(cwd);
|
|
246
|
+
if (rootSignals.frontend) shape.frontend = true;
|
|
247
|
+
if (rootSignals.backend) shape.backend = true;
|
|
248
|
+
|
|
249
|
+
shape.workspacePackages = readWorkspacePackageDirs(cwd);
|
|
250
|
+
for (const relative of shape.workspacePackages) {
|
|
251
|
+
const side = shapeFromDirName(path.posix.basename(relative));
|
|
252
|
+
if (side) shape[side] = true;
|
|
253
|
+
|
|
254
|
+
const signals = directorySignals(path.join(cwd, relative));
|
|
255
|
+
if (signals.frontend) shape.frontend = true;
|
|
256
|
+
if (signals.backend) shape.backend = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return shape;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function detectProfileName(cwd, intent) {
|
|
263
|
+
const normalizedIntent = typeof intent === "string" ? intent.trim().toLowerCase() : "";
|
|
264
|
+
if (normalizedIntent === "be-readonly-fe") {
|
|
265
|
+
return { name: "be-readonly-fe", reason: "User intent says backend should be read-only while frontend is the write target." };
|
|
266
|
+
}
|
|
267
|
+
if (normalizedIntent === "frontend-only") return { name: "web-frontend", reason: "User intent says frontend-only." };
|
|
268
|
+
if (normalizedIntent === "backend-only") return { name: "backend-api", reason: "User intent says backend-only." };
|
|
269
|
+
if (normalizedIntent === "docs") return { name: "docs", reason: "User intent says docs/docs-only." };
|
|
270
|
+
|
|
271
|
+
const shape = detectProjectShape(cwd);
|
|
272
|
+
|
|
273
|
+
if (shape.mobile) return { name: "mobile", reason: "Mobile markers found." };
|
|
274
|
+
if (shape.frontend && shape.backend) {
|
|
275
|
+
return { name: "fullstack", reason: "Frontend and backend markers both found. Pick be-readonly-fe instead if backend must be read-only." };
|
|
276
|
+
}
|
|
277
|
+
if (shape.frontend) return { name: "web-frontend", reason: "Frontend framework markers found." };
|
|
278
|
+
if (shape.backend) return { name: "backend-api", reason: "Backend/API markers found." };
|
|
279
|
+
if (shape.data) return { name: "data", reason: "Data/ETL markers found." };
|
|
280
|
+
if (exists(cwd, "pyproject.toml")) return { name: "python", reason: "Python pyproject.toml found." };
|
|
281
|
+
if (shape.hasPackage && exists(cwd, "tsconfig.json")) return { name: "node-typescript", reason: "Node TypeScript markers found." };
|
|
282
|
+
if (shape.infra) return { name: "devops", reason: "Infrastructure markers found." };
|
|
283
|
+
if (shape.docs) return { name: "docs", reason: "Documentation markers found." };
|
|
284
|
+
return { name: "generic", reason: "No stronger project markers found." };
|
|
285
|
+
}
|
package/scripts/init-project.sh
CHANGED
|
@@ -102,97 +102,20 @@ fi
|
|
|
102
102
|
|
|
103
103
|
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[[ -f "$project/package.json" ]] && grep -Eiq "$pattern" "$project/package.json"
|
|
109
|
-
}
|
|
110
|
-
|
|
105
|
+
# Profile detection lives in packages/piagent-core/extensions/project-shape.js so
|
|
106
|
+
# that this script and the runtime `piagent_profile_options` tool recommend the
|
|
107
|
+
# same profile. They used to hold separate copies of the rules and drifted apart.
|
|
111
108
|
detect_profile() {
|
|
112
109
|
local project="$1"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
[[ -f "$project/package.json" ]] && has_package=true
|
|
123
|
-
|
|
124
|
-
if [[ -f "$project/pubspec.yaml" ]] || { [[ -d "$project/android" ]] && [[ -d "$project/ios" ]]; }; then
|
|
125
|
-
mobile=true
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
if [[ -f "$project/dbt_project.yml" ]] || [[ -f "$project/dvc.yaml" ]] || [[ -d "$project/notebooks" ]] || [[ -d "$project/data" ]]; then
|
|
129
|
-
data=true
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
if [[ -f "$project/Dockerfile" ]] || [[ -f "$project/docker-compose.yml" ]] || [[ -f "$project/compose.yml" ]] || [[ -f "$project/compose.yaml" ]] || [[ -d "$project/terraform" ]] || [[ -d "$project/infra" ]] || [[ -d "$project/k8s" ]] || [[ -d "$project/helm" ]]; then
|
|
133
|
-
infra=true
|
|
134
|
-
devops=true
|
|
135
|
-
elif [[ -d "$project/.github/workflows" ]]; then
|
|
136
|
-
devops=true
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
if [[ -d "$project/docs" ]] || [[ -f "$project/mkdocs.yml" ]] || [[ -f "$project/mint.json" ]] || [[ -f "$project/docusaurus.config.js" ]]; then
|
|
140
|
-
docs=true
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
if [[ "$has_package" == true ]]; then
|
|
144
|
-
if [[ -d "$project/frontend" ]] || [[ -d "$project/apps/web" ]] || [[ -d "$project/apps/frontend" ]]; then
|
|
145
|
-
frontend=true
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
if [[ -d "$project/backend" ]] || [[ -d "$project/apps/api" ]] || [[ -d "$project/apps/server" ]]; then
|
|
149
|
-
backend=true
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
if package_has_any "$project" '"(next|react|vite|vue|svelte|astro|@angular/core|remix)"' \
|
|
153
|
-
|| [[ -f "$project/next.config.js" ]] || [[ -f "$project/next.config.mjs" ]] || [[ -f "$project/next.config.ts" ]] \
|
|
154
|
-
|| [[ -f "$project/vite.config.js" ]] || [[ -f "$project/vite.config.ts" ]] \
|
|
155
|
-
|| [[ -d "$project/src/app" ]] || [[ -d "$project/pages" ]] || [[ -d "$project/public" ]]; then
|
|
156
|
-
frontend=true
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
if package_has_any "$project" '"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"' \
|
|
160
|
-
|| [[ -f "$project/nest-cli.json" ]] || [[ -d "$project/prisma" ]] || [[ -d "$project/src/server" ]] || [[ -d "$project/src/api" ]]; then
|
|
161
|
-
backend=true
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
if [[ -f "$project/pom.xml" ]] || [[ -f "$project/build.gradle" ]] || [[ -f "$project/build.gradle.kts" ]] || [[ -d "$project/src/main/java" ]] || [[ -d "$project/src/main/kotlin" ]]; then
|
|
166
|
-
backend=true
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
if [[ -f "$project/pyproject.toml" ]] && grep -Eiq "(fastapi|flask|django|litestar|starlite)" "$project/pyproject.toml"; then
|
|
170
|
-
backend=true
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
if [[ "$mobile" == true ]]; then
|
|
174
|
-
printf '%s\n' "mobile"
|
|
175
|
-
elif [[ "$frontend" == true && "$backend" == true ]]; then
|
|
176
|
-
printf '%s\n' "fullstack"
|
|
177
|
-
elif [[ "$frontend" == true ]]; then
|
|
178
|
-
printf '%s\n' "web-frontend"
|
|
179
|
-
elif [[ "$backend" == true ]]; then
|
|
180
|
-
printf '%s\n' "backend-api"
|
|
181
|
-
elif [[ "$data" == true ]]; then
|
|
182
|
-
printf '%s\n' "data"
|
|
183
|
-
elif [[ -f "$project/pyproject.toml" ]]; then
|
|
184
|
-
printf '%s\n' "python"
|
|
185
|
-
elif [[ "$has_package" == true && -f "$project/tsconfig.json" ]]; then
|
|
186
|
-
printf '%s\n' "node-typescript"
|
|
187
|
-
elif [[ "$infra" == true ]]; then
|
|
188
|
-
printf '%s\n' "devops"
|
|
189
|
-
elif [[ "$docs" == true ]]; then
|
|
190
|
-
printf '%s\n' "docs"
|
|
191
|
-
elif [[ "$devops" == true ]]; then
|
|
192
|
-
printf '%s\n' "devops"
|
|
193
|
-
else
|
|
194
|
-
printf '%s\n' "generic"
|
|
195
|
-
fi
|
|
110
|
+
node --input-type=module - "$PLATFORM_ROOT" "$project" <<'NODE'
|
|
111
|
+
import path from "node:path";
|
|
112
|
+
import { pathToFileURL } from "node:url";
|
|
113
|
+
|
|
114
|
+
const [platformRoot, project] = process.argv.slice(2);
|
|
115
|
+
const modulePath = path.join(platformRoot, "packages/piagent-core/extensions/project-shape.js");
|
|
116
|
+
const { detectProfileName } = await import(pathToFileURL(modulePath).href);
|
|
117
|
+
process.stdout.write(`${detectProfileName(project).name}\n`);
|
|
118
|
+
NODE
|
|
196
119
|
}
|
|
197
120
|
|
|
198
121
|
resolve_profile_path() {
|
|
@@ -15,7 +15,7 @@ Package source examples:
|
|
|
15
15
|
# Installs the stable tag after resolving it to a commit SHA.
|
|
16
16
|
|
|
17
17
|
# Pin a specific release:
|
|
18
|
-
scripts/install-global.sh --version v1.1.
|
|
18
|
+
scripts/install-global.sh --version v1.1.6 --resolve-tag
|
|
19
19
|
|
|
20
20
|
# Preview the planned install/update without changing user config:
|
|
21
21
|
scripts/install-global.sh --stable --dry-run
|
|
@@ -127,7 +127,7 @@ detect_runtime_surface() {
|
|
|
127
127
|
printf 'native-windows/%s (not release-gated; terminal helpers require Bash semantics)' "$arch_name"
|
|
128
128
|
;;
|
|
129
129
|
*)
|
|
130
|
-
printf '%s/%s (outside v1.1.
|
|
130
|
+
printf '%s/%s (outside v1.1.6 release matrix)' "$os_name" "$arch_name"
|
|
131
131
|
;;
|
|
132
132
|
esac
|
|
133
133
|
}
|
|
@@ -135,7 +135,7 @@ detect_runtime_surface() {
|
|
|
135
135
|
warn_runtime_surface_if_needed() {
|
|
136
136
|
local surface="$1"
|
|
137
137
|
case "$surface" in
|
|
138
|
-
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.
|
|
138
|
+
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.6 release matrix"*)
|
|
139
139
|
echo "WARN: runtime surface is $surface." >&2
|
|
140
140
|
echo "WARN: For team rollout, run piagent-doctor plus the project smoke/verify suite on this machine before relying on it." >&2
|
|
141
141
|
;;
|
package/scripts/piagent-cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const scriptByCommand = {
|
|
7
7
|
"piagent-setup": "scripts/setup.sh",
|
|
8
8
|
"piagent-install": "scripts/install-global.sh",
|
|
9
|
+
"piagent-update": "scripts/update-global.mjs",
|
|
9
10
|
"piagent-uninstall": "scripts/uninstall-global.sh",
|
|
10
11
|
"piagent-init": "scripts/init-project.sh",
|
|
11
12
|
"piagent-doctor": "scripts/team-doctor.sh",
|
package/scripts/team-doctor.sh
CHANGED
|
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
|
|
|
137
137
|
surface: `${platform}/${arch}`,
|
|
138
138
|
status: "outside-release-matrix",
|
|
139
139
|
teamRolloutReady: false,
|
|
140
|
-
note: "This runtime is outside the documented v1.1.
|
|
140
|
+
note: "This runtime is outside the documented v1.1.6 support matrix."
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One command that moves a machine from one release to the next.
|
|
3
|
+
//
|
|
4
|
+
// A release is three independently versioned things: the Pi host, this npm-global
|
|
5
|
+
// terminal helper, and the Pi package the helper installs. They have to move in
|
|
6
|
+
// that order, because `piagent-install` refuses to run against a host that does
|
|
7
|
+
// not match the version its own package.json pins. Doing it by hand means three
|
|
8
|
+
// commands and one ordering rule, and getting the order wrong fails late.
|
|
9
|
+
//
|
|
10
|
+
// The awkward part is that the helper is the program running this file, so it has
|
|
11
|
+
// to replace itself. Rather than re-exec a half-replaced process, every version
|
|
12
|
+
// this run needs is resolved from the registry *before* anything is installed:
|
|
13
|
+
// the target helper version and, from that version's metadata, the host version it
|
|
14
|
+
// will demand. The plan is therefore complete and printable before the first
|
|
15
|
+
// mutation, `--dry-run` shows exactly what a real run would do, and after the
|
|
16
|
+
// helper is replaced the run only has to confirm that what landed on disk is what
|
|
17
|
+
// it asked for.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
const HELPER_PACKAGE = "@piagent/platform";
|
|
25
|
+
const HOST_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
26
|
+
const EXIT_USAGE = 2;
|
|
27
|
+
|
|
28
|
+
const platformRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
|
|
30
|
+
function usage() {
|
|
31
|
+
return `Usage:
|
|
32
|
+
piagent-update [options]
|
|
33
|
+
|
|
34
|
+
Updates the Pi host, this terminal helper, and the Pi package together, in the
|
|
35
|
+
order a release requires.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--check Report current and available versions, change nothing
|
|
39
|
+
--dry-run Print every command that would run, change nothing
|
|
40
|
+
--version <x.y.z> Update to an exact helper version instead of latest
|
|
41
|
+
--force Reinstall even when already on the target version
|
|
42
|
+
--project <path> Run the team doctor against this project afterwards
|
|
43
|
+
--no-host Do not touch the Pi host
|
|
44
|
+
--no-package Do not reinstall the Pi package
|
|
45
|
+
-h, --help Show this help
|
|
46
|
+
|
|
47
|
+
Anything after -- is passed through to piagent-install, for example:
|
|
48
|
+
piagent-update -- --no-mcp
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fail(message, code = 1) {
|
|
53
|
+
console.error(`FAIL: ${message}`);
|
|
54
|
+
process.exit(code);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readJson(file) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isReleaseVersion(value) {
|
|
66
|
+
return typeof value === "string" && /^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$/.test(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// "v1.2.3" and "1.2.3" both name the same release everywhere else in this
|
|
70
|
+
// repository, so accept either and normalise once.
|
|
71
|
+
function normalizeVersion(value) {
|
|
72
|
+
return typeof value === "string" && value.startsWith("v") ? value.slice(1) : value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseArguments(argv) {
|
|
76
|
+
const options = {
|
|
77
|
+
check: false,
|
|
78
|
+
dryRun: false,
|
|
79
|
+
force: false,
|
|
80
|
+
version: undefined,
|
|
81
|
+
project: undefined,
|
|
82
|
+
host: true,
|
|
83
|
+
package: true,
|
|
84
|
+
passthrough: []
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
88
|
+
const argument = argv[index];
|
|
89
|
+
if (argument === "--") {
|
|
90
|
+
options.passthrough = argv.slice(index + 1);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
switch (argument) {
|
|
94
|
+
case "-h":
|
|
95
|
+
case "--help":
|
|
96
|
+
process.stdout.write(usage());
|
|
97
|
+
process.exit(0);
|
|
98
|
+
break;
|
|
99
|
+
case "--check":
|
|
100
|
+
options.check = true;
|
|
101
|
+
break;
|
|
102
|
+
case "--dry-run":
|
|
103
|
+
options.dryRun = true;
|
|
104
|
+
break;
|
|
105
|
+
case "--force":
|
|
106
|
+
options.force = true;
|
|
107
|
+
break;
|
|
108
|
+
case "--no-host":
|
|
109
|
+
options.host = false;
|
|
110
|
+
break;
|
|
111
|
+
case "--no-package":
|
|
112
|
+
options.package = false;
|
|
113
|
+
break;
|
|
114
|
+
case "--version": {
|
|
115
|
+
const value = normalizeVersion(argv[index += 1]);
|
|
116
|
+
if (!isReleaseVersion(value)) fail(`--version needs an exact release such as 1.1.4, got ${argv[index] ?? "nothing"}`, EXIT_USAGE);
|
|
117
|
+
options.version = value;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "--project": {
|
|
121
|
+
const value = argv[index += 1];
|
|
122
|
+
if (!value) fail("--project needs a path", EXIT_USAGE);
|
|
123
|
+
options.project = value;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
fail(`unknown option: ${argument}\n\n${usage()}`, EXIT_USAGE);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function run(command, args, { capture = false } = {}) {
|
|
135
|
+
const result = spawnSync(command, args, {
|
|
136
|
+
encoding: "utf8",
|
|
137
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
138
|
+
});
|
|
139
|
+
if (result.error) return { ok: false, stdout: "", stderr: String(result.error.message ?? result.error) };
|
|
140
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function npmView(specifier, field) {
|
|
144
|
+
const result = run("npm", ["view", specifier, field, "--json"], { capture: true });
|
|
145
|
+
if (!result.ok) return undefined;
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(result.stdout.trim());
|
|
148
|
+
} catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function installedHostVersion() {
|
|
154
|
+
const result = run("pi", ["--version"], { capture: true });
|
|
155
|
+
if (!result.ok) return undefined;
|
|
156
|
+
const match = /([0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?)/.exec(result.stdout);
|
|
157
|
+
return match ? match[1] : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The helper installs itself from the registry, so a run started from a git
|
|
161
|
+
// checkout would replace the checkout's own commands with a published build and
|
|
162
|
+
// leave the operator somewhere they did not ask to be.
|
|
163
|
+
function runningFromGlobalInstall() {
|
|
164
|
+
return platformRoot.split(path.sep).includes("node_modules");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function describeStep(dryRun, command, args) {
|
|
168
|
+
console.log(`${dryRun ? "DRY RUN:" : "run:"} ${command} ${args.join(" ")}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function main() {
|
|
172
|
+
const options = parseArguments(process.argv.slice(2));
|
|
173
|
+
|
|
174
|
+
const manifest = readJson(path.join(platformRoot, "package.json"));
|
|
175
|
+
if (!manifest?.version) fail(`cannot read the helper package at ${platformRoot}`);
|
|
176
|
+
const currentHelper = manifest.version;
|
|
177
|
+
const currentHost = installedHostVersion();
|
|
178
|
+
|
|
179
|
+
const targetHelper = options.version ?? npmView(HELPER_PACKAGE, "version");
|
|
180
|
+
if (!isReleaseVersion(targetHelper)) {
|
|
181
|
+
fail(`cannot resolve a release for ${HELPER_PACKAGE}. Check network access to the npm registry, or pass --version.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The host requirement belongs to the version being installed, not the one
|
|
185
|
+
// running, so it is read from the target's published metadata.
|
|
186
|
+
const targetPeers = npmView(`${HELPER_PACKAGE}@${targetHelper}`, "peerDependencies");
|
|
187
|
+
const targetHost = targetPeers?.[HOST_PACKAGE];
|
|
188
|
+
if (!isReleaseVersion(targetHost)) {
|
|
189
|
+
fail(`${HELPER_PACKAGE}@${targetHelper} does not pin an exact ${HOST_PACKAGE} version, so the host cannot be updated safely.`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const helperNeedsChange = options.force || currentHelper !== targetHelper;
|
|
193
|
+
const hostNeedsChange = options.host && (options.force || currentHost !== targetHost);
|
|
194
|
+
|
|
195
|
+
console.log("Pi Agent Platform update");
|
|
196
|
+
console.log(` helper: ${currentHelper} -> ${targetHelper}${helperNeedsChange ? "" : " (already current)"}`);
|
|
197
|
+
console.log(` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
|
|
198
|
+
console.log(` package: reinstalled from v${targetHelper} as a resolved commit${options.package ? "" : " (skipped)"}`);
|
|
199
|
+
|
|
200
|
+
if (options.check) {
|
|
201
|
+
const pending = [helperNeedsChange && "helper", hostNeedsChange && "host"].filter(Boolean);
|
|
202
|
+
console.log(pending.length ? `\nUpdate available: ${pending.join(", ")}.` : "\nEverything is on the target release.");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!helperNeedsChange && !hostNeedsChange && !options.package) {
|
|
207
|
+
console.log("\nNothing to do.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!options.dryRun && !runningFromGlobalInstall()) {
|
|
212
|
+
fail(`this command updates the npm-global helper, but it is running from ${platformRoot}.\n`
|
|
213
|
+
+ "From a checkout, update with git and install from it:\n"
|
|
214
|
+
+ ` git -C ${platformRoot} pull\n`
|
|
215
|
+
+ ` bash ${path.join(platformRoot, "scripts/install-global.sh")} --local`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Host first: piagent-install refuses to run against a host that does not match
|
|
219
|
+
// the version its package.json pins, so a helper installed ahead of the host
|
|
220
|
+
// would fail on its very next step.
|
|
221
|
+
if (hostNeedsChange) {
|
|
222
|
+
const args = ["install", "-g", "--ignore-scripts", `${HOST_PACKAGE}@${targetHost}`];
|
|
223
|
+
describeStep(options.dryRun, "npm", args);
|
|
224
|
+
if (!options.dryRun && !run("npm", args).ok) fail(`could not install ${HOST_PACKAGE}@${targetHost}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (helperNeedsChange) {
|
|
228
|
+
const args = ["install", "-g", "--ignore-scripts", `${HELPER_PACKAGE}@${targetHelper}`];
|
|
229
|
+
describeStep(options.dryRun, "npm", args);
|
|
230
|
+
if (!options.dryRun) {
|
|
231
|
+
if (!run("npm", args).ok) fail(`could not install ${HELPER_PACKAGE}@${targetHelper}`);
|
|
232
|
+
// The helper has just replaced itself on disk. Everything after this point
|
|
233
|
+
// runs the newly installed scripts, so confirm they are the ones asked for
|
|
234
|
+
// rather than trusting that npm did what it said.
|
|
235
|
+
const landed = readJson(path.join(platformRoot, "package.json"))?.version;
|
|
236
|
+
if (landed !== targetHelper) {
|
|
237
|
+
fail(`installed ${HELPER_PACKAGE}@${targetHelper} but ${platformRoot} reports ${landed ?? "nothing"}. `
|
|
238
|
+
+ "The Pi package was left untouched; rerun once the helper is correct.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (options.package) {
|
|
244
|
+
const installer = path.join(platformRoot, "scripts/install-global.sh");
|
|
245
|
+
if (!options.dryRun && !fs.existsSync(installer)) {
|
|
246
|
+
fail(`the updated helper has no installer at ${installer}`);
|
|
247
|
+
}
|
|
248
|
+
const args = [installer, "--stable", ...options.passthrough];
|
|
249
|
+
describeStep(options.dryRun, "bash", args);
|
|
250
|
+
if (!options.dryRun && !run("bash", args).ok) fail("the Pi package install did not complete");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.project) {
|
|
254
|
+
const doctor = path.join(platformRoot, "scripts/team-doctor.sh");
|
|
255
|
+
const args = [doctor, options.project, "--strict-share"];
|
|
256
|
+
describeStep(options.dryRun, "bash", args);
|
|
257
|
+
if (!options.dryRun && !run("bash", args).ok) fail("the team doctor reported a problem");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log(options.dryRun
|
|
261
|
+
? `\nDRY RUN: nothing was installed. Rerun without --dry-run to apply v${targetHelper}.`
|
|
262
|
+
: `\nPASS: updated to v${targetHelper}.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
main();
|