pi-codex-marketplace 0.7.0 → 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/README.md CHANGED
@@ -1,189 +1,52 @@
1
1
  # pi-codex-marketplace
2
2
 
3
- Bridge Package for Codex and Claude Marketplace compatibility in Pi (`Pi 0.84.2`).
3
+ Bridge Package for Codex and Claude Marketplace compatibility in Pi (`Pi 0.84.2`)。
4
4
 
5
- > **One-line:** `pi install npm:pi-codex-marketplace` → `/codex-marketplace add <本機資料夾|GitHub 網址>` 註冊 marketplace → `install <編號>` 裝到當下最新並立刻在 Pi 可用。
5
+ > **One-line:** `pi install npm:pi-codex-marketplace` → `/codex-marketplace add <本機資料夾|GitHub 網址>`(或 Shell 下 `npx pi-codex-marketplace add ...`)註冊 marketplace → `install <編號>` 裝到當下最新並在 Pi 可用。
6
6
 
7
- ## Install
7
+ ## Quickstart
8
8
 
9
9
  ```bash
10
- pi install npm:pi-codex-marketplace # Global package (writes to ~/.pi/agent/settings.json)
11
- pi install npm:pi-codex-marketplace -l # Project package location (.pi/settings.json; Pi host package setting only)
12
- pi install ./path/to/pi-codex-marketplace # Local path (try without publishing)
13
- pi install ./path/to/pi-codex-marketplace -l # Local path, Project package location
14
- pi -e npm:pi-codex-marketplace # Ephemeral try without installing (temporary)
15
- pi update npm:pi-codex-marketplace # Update one package
16
- pi update --all # Update pi + all packages
17
- pi remove npm:pi-codex-marketplace # Remove package
18
- pi list # List installed packages
19
- pi config # Enable/disable resources
20
- ```
21
-
22
- Source types follow `docs/packages.md`: `npm:` for registry, `git:`/`https://` for git, and absolute/relative paths for local. Ephemeral runs use `pi -e <source>` (not `pi install -e`). This package declares a single `pi` extension entry (`extensions/pi/index.ts`) loaded via `jiti` and requires no build step.
10
+ # 1. 安裝(擇一表面)
11
+ pi install npm:pi-codex-marketplace # Pi Extension(TUI/互動環境)
12
+ npm install -g pi-codex-marketplace # Headless CLI(Shell/CI;亦可 npx 免安裝)
23
13
 
24
- > [!NOTE]
25
- > `pi install -l` controls where Pi loads this Bridge Package extension from (`.pi/settings.json` vs `~/.pi/agent/settings.json` on the Pi host). Regardless of how Pi installs the package, the Bridge State managed by `pi-codex-marketplace` is always recorded in the single Global Scope (`~/.pi/agent/codex-marketplace/state.json`).
14
+ # 2. 註冊 marketplace
15
+ /codex-marketplace add <本機資料夾|GitHub 網址> # TUI
16
+ npx pi-codex-marketplace add <本機資料夾|GitHub 網址> # Shell 下
26
17
 
27
- Requirements: **Pi 0.84.2**, **Node >=22.19.0**, **macOS / Linux** (Windows not supported).
18
+ # 3. 裝到當下最新並自動啟用
19
+ /codex-marketplace install <編號|名稱>
20
+ ```
28
21
 
29
- ## Usage `/codex-marketplace`(純文字,無 TUI)
22
+ Requirements: **Pi 0.84.2** · **Node >=22.19.0** · **macOS / Linux**(Windows not supported)。完整安裝方式見 [docs/installation.md](./docs/installation.md)。
30
23
 
31
- Nine subcommands, no arguments = 總覽:
24
+ ## 子命令速查
32
25
 
33
- ```
34
- /codex-marketplace
35
- /codex-marketplace help
36
- ```
26
+ TUI `/codex-marketplace` 與 Headless CLI `pi-codex-marketplace` 共用同一組九個子命令(無參數=總覽):
37
27
 
38
28
  | 子命令 | 行為 |
39
29
  |--------|------|
40
- | `add <路徑\|網址>` | 註冊 marketplace(本機資料夾、GitHub 完整網址、`owner/repo` 簡寫皆收),自動偵測 codex/claude 格式並告知 `偵測:<format> marketplace · N plugins`。重複註冊同來源被拒絕並提示下一步。Git 來源以安全線取得(`core.hooksPath=/dev/null`、`GIT_LFS_SKIP_SMUDGE=1`、`GIT_TERMINAL_PROMPT=0`),catalog 解析失敗明示錯誤、不註冊。私有 HTTPS repo **開箱即用**:自動偵測本機憑證來源(`gh` 登入/macOS 鑰匙圈/`credential-store`)並逐次核准;也可用 `PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS` 顯式覆蓋核准清單(逐次生效、永不持久化),或改用 SSH 定位器;用法與範例見下方〈私有 Git repo:Credentialed Acquisition〉。 |
41
- | `list [名稱]` | 列出 plugins(編號/所屬 marketplace/狀態:可安裝・已裝啟用・已裝停用・unavailable+原因),可帶 marketplace 名稱過濾。 |
42
- | `install <編號\|名稱>` | 裝到**當下最新**並自動啟用+reload。成功話術如 `安裝 "name"(N skills:a, b, c)· 已重新載入生效`;同名衝突列出 `⚠ skill "b" 與既有同名,未投影(名稱衝突)`。**重複安裝=重抓最新覆寫**(重裝=更新,不報錯)。 |
43
- | `update` | 對全部已註冊 marketplace 重抓最新:有變化的 plugin 升到最新、無變化各自顯示「無變化」;整體以「已重新載入生效」收尾(有變時)。 |
44
- | `disable <名稱>` / `enable <名稱>` | 停用/啟用 plugin(enable 重新投影+reload)。 |
45
- | `remove <名稱>` | 移除單支 plugin(不動 marketplace、不動來源資料)。 |
46
- | `forget <名稱>` | 移除整個 marketplace(含其全部安裝)。 |
47
- | `help` | 子命令清單。 |
48
-
49
- 語意鐵則:
50
-
51
- - 安裝語意不分「安裝/啟用」兩步;輸出不得宣稱 reload skill 已在 host 內可見(host 無內省 API),只說「已重新載入生效」。
52
- - catalog 內 git 型或不支援來源的 entry 一律 `unavailable` 並顯示原因;解析失敗顯示明確錯誤、不給裝。
53
- - 安裝成功後由指令層主動要求 reload;reload 失敗不影響已記錄狀態,下次 session start 或 `/reload` 仍生效。
54
- - `--no-skills` 啟動 Pi 不影響 Bridge 投影。
55
-
56
- ### Autocomplete(Pi 原生,TUI 限定)
57
-
58
- 互動(TUI)模式下,`/codex-marketplace` **Pi 原生 autocomplete** 提供兩層、狀態感知的候選。**純文字指令表面維持權威不變**:九個子命令、指令參數、輸出與語意完全不受 autocomplete 影響;RPC/JSON/print 模式根本不註冊 terminal-only provider。
59
-
60
- **第一層——九個根層子命令。** 輸入完整的 `/codex-marketplace` 後按 Tab,候選清單顯示全部九個子命令(`add`/`list`/`install`/`update`/`disable`/`enable`/`remove`/`forget`/`help`)與各自說明,支援不分大小寫的模糊搜尋。選取需要參數的子命令(`add`/`list`/`install`/`disable`/`enable`/`remove`/`forget`)會自動補上一個尾隨空格,可直接繼續輸入;`update` 與 `help` 不加。
61
-
62
- **第二層——再按一次 Tab 開啟狀態感知候選。** 需要參數的子命令套用後,**再按一次 Tab** 依當下 Bridge State 只列出當下可執行的選項(空集合不給假候選);**不承諾自動重開 selector**(Pi 0.84.2 在套用候選後不會自動再開一層補完選單,鍵盤流程固定是「輸入 command → Tab 選子命令 → 需要參數時再按一次 Tab」):
63
-
64
- | 子命令 | 候選範圍 | 歧義處理 |
65
- |--------|----------|----------|
66
- | `install` | 可安裝/可重裝的 plugin(**不含 Unavailable Entry**) | 名稱在完整 enumeration 唯一=插入名稱;同名(含 unavailable sibling)=插入 enumeration 編號(`#N`),描述顯示 `[marketplace]` 與狀態 |
67
- | `enable` | 僅**已停用**的 Installation | 名稱無法唯一解析的記錄不給候選 |
68
- | `disable` | 僅**已啟用**的 Installation | 同上 |
69
- | `remove` | 全部已安裝 plugin(不分啟用/停用) | 同上 |
70
- | `list` | Marketplace Registrations | 名稱無法唯一解析=依序改插唯一可解析的 alias、其次 Registration id |
71
- | `forget` | Marketplace Registrations | 同上 |
72
- | `add` | **不提供 Bridge 候選**:Tab 委派 Pi 原生路徑 completion,Git locator 維持自由輸入 | — |
73
-
74
- 補完只提議當下可執行的動作,候選反映最新 Bridge State,且**被動讀取**——按 Tab 絕不會重置或重寫損壞的 state 文件。其餘輸入(其他 slash 指令、一般文字、檔案/路徑補完)一律原樣委派 Pi 既有 provider;安裝本套件不影響任何其他指令的 autocomplete。
75
-
76
- > 沒有 custom TUI、沒有自動第二層 selector:所有操作也都可以照舊以純文字輸入完成,autocomplete 只是 discoverability 與輸入效率層。
77
-
78
- ### 私有 Git repo:Credentialed Acquisition(核准式取得)
79
-
80
- 對私有 HTTPS repo,`add`/`update` **開箱即用**:預設會自動偵測本機已存在的憑證來源並逐次核准(**固定白名單**,不讀本機 gitconfig 的任意 helper;偵測結果只限該次呼叫、永不持久化):
81
-
82
- | 偵測來源 | 核准的 credential helper |
83
- |---|---|
84
- | `gh` CLI 已登入(`gh auth status` 成功) | `!gh auth git-credential` |
85
- | macOS 原生鑰匙圈 helper 存在 | `osxkeychain` |
86
- | git `credential-store` 存在 | `store`(明文憑證檔,請知悉風險) |
87
-
88
- 已註冊或欲手動控制時,以逗號分隔設定環境變數——**顯式設定完全覆蓋自動偵測**(逐次生效、永不持久化,`add` 與 `update` 共用同一核准來源):
89
-
90
- ```
91
- PI_CODEX_MARKETPLACE_CREDENTIAL_HELPERS='store, !f() { echo "username=${GITHUB_USER}"; echo "password=${GITHUB_TOKEN}"; }; f'
92
- /codex-marketplace add https://github.com/acme/private-mkt
93
- ```
94
-
95
- Credentialed Acquisition 語意(安全線):
96
-
97
- - 預設(未設 env)=自動偵測固定白名單(gh/osxkeychain/store),僅限該次呼叫:**不寫入** Bridge State、設定檔或任何持久化位置;偵測到的 helper 以命令列 `credential.helper=` 傳入,gitconfig 的其他 helper(含 URL-scoped)仍被排除。
98
- - 設定 env 後完全覆蓋:值為 git `credential.helper` 字串,逗號分隔、各項 trim、空項目忽略;未設定且偵測無結果=無任何 helper(行為與 credential-free 完全相同,安全線其餘禁制不變)。
99
- - 憑證與核准清單**永不**進入指令輸出、Bridge State、Canonical Git Locator(定位器)、Validation Snapshot(快照)或 cache identity(快取身份)——取得流程的 identity 判定與憑證完全無關。
100
- - 自動偵測或已核准的 helper 仍被遠端拒絕(401)時,錯誤訊息提示檢查登入(`gh auth status`/keychain)或設環境變數核准其他 helper;本機偵測不到任何憑證來源時提示設環境變數或改用 SSH。
101
- - 設定的 helper 名稱無效時(例如直接寫 `gh`——它不是 git 原生的 credential helper 執行檔),錯誤訊息指出正確寫法:原生 helper 名稱(`osxkeychain`/`store`)或 shell form(`!gh auth git-credential`)。
102
-
103
- #### SSH 定位器:私有 repo 的替代路徑
104
-
105
- 私有 repo 可以完全繞過此環境變數,直接用 SSH 定位器註冊(HTTPS 與 SSH 同屬允許的 credential-free 定位器):
106
-
107
- ```
108
- /codex-marketplace add git@github.com:acme/private-mkt # scp-like 簡寫(canonical:ssh://git@github.com/acme/private-mkt)
109
- /codex-marketplace add ssh://git@github.com/acme/private-mkt
110
- ```
111
-
112
- 前提(與既有 Acquisition Trust Base 一致):
113
-
114
- - host key 必須**預先存在** `~/.ssh/known_hosts`——安全線以 `StrictHostKeyChecking=yes` 只信任既有 host key,遇到未知或變更的主機金鑰直接拒絕,不會提示接受;
115
- - 憑證由 SSH agent 提供,整個取得過程不互動(`BatchMode=yes`)、無任何提示;
116
- - SSH 定位器本身仍維持 credential-free:不得內嵌密碼(`user:pass@` 拒絕);憑證只能經由 SSH agent 或 Credentialed Acquisition 到達取得流程。
117
-
118
- ## Bridge State storage
119
-
120
- Bridge State 是唯一權威,存於**單一 Global Scope 文件** `{getAgentDir()}/codex-marketplace/state.json`(`~/.pi/agent/codex-marketplace/state.json`):
121
-
122
- ```jsonc
123
- {
124
- "schemaVersion": 1, // 固定,永不遷移
125
- "registrations": [], // immutable Registration ID = UUIDv4;sourceKind: "local" | "git";git 帶 snapshot fingerprint(cache 位址)
126
- "installations": [] // Installed Plugins(enabled/disabled),含 manifestName、skills 與 snapshot(git)
127
- }
128
- ```
129
-
130
- - **重建=重置**:壞檔或不認識的格式一律重置為空、重新註冊重裝;沒有 repair、沒有 migration、沒有 State Revision。
131
- - 寫入防護:`write-to-temp → fsync → rename`(原子)+ 檔案鎖(`.lock` sibling,last-write-wins、無 stale 偵測)+ read-after-verify。
132
- - Git marketplace 的 snapshot fingerprint 是 **Source Cache 位址鑰匙**(`cache/entries/<fingerprint>`);投影直讀該 cache entry,指紋不可替換成別種身份值。Cache 只對非 pinned entry 做 LRU 驅逐。
133
-
134
- See `src/bridge/state.ts`(Minimal Bridge State)、`src/bridge-state/atomic.ts`(原子寫入+檔案鎖)、`src/cache/source-cache.ts`。
135
-
136
- ## Support matrix
137
-
138
- | Dimension | Supported | Notes |
139
- |-----------|-----------|-------|
140
- | OS | **macOS**, **Linux** | Windows not supported (path containment, symlink, `flock` semantics are POSIX-only) |
141
- | Node | **>=22.19.0** | `engines.node` enforced |
142
- | Pi host | **0.84.2** | `peerDependencies` exact `0.84.2`; expected compatible range `^0.84.2` (devDeps). `pi-ai`/`pi-tui` peers `*` per Pi extension docs. |
143
- | Semantics | `pi install` / `pi -e` / `pi install -l` / `pi update` / `pi remove` / `pi list` / `pi config` | Single `pi` extension package; `files` ships `extensions/`, `src/`, `README.md`, `LICENSE` only |
144
-
145
- Peer declaration (dual): **精確 `0.84.2`** in `peerDependencies` (exact host that this version was validated against) + **預期 `^0.84.2`** in `devDependencies` (range expected to remain compatible). `pi-ai` and `pi-tui` remain `*` because they are bundled by Pi.
146
-
147
- ## Versioning & release flow
148
-
149
- - **Package**: `pi-codex-marketplace` published to **npm** as primary, **Git tag** `v*` as mirror.
150
- - **SemVer**: starts at `0.1.0`; `0.y` maintenance window until `1.0.0` signals a stable Bridge State contract.
151
- - **Publishing**: `v*` tag → CI **full matrix green** (below) is a **release gate** → `npm publish --provenance` (OIDC). `latest` tracks stable tags (`v0.*` stable line and later `v1.*`); `next` tracks pre-release tags. Provenance is required (`--provenance`) and verified post-publish by the publish workflow. See `.github/workflows/ci.yml` and `.github/workflows/publish.yml`.
152
-
153
- ## Verification matrix (發版阻擋 gate)
154
-
155
- Every row is a **release blocker**: `v*` may not publish unless the full matrix is green.
156
-
157
- | Layer | What is covered |
158
- |-------|-----------------|
159
- | unit — 縫層 | `runCommand` 指令分派(add/list/install/update/disable/enable/remove/forget/help、重複註冊拒絕、重裝覆寫、衝突未投影清單、corrupt→重置、unavailable 顯示、git 重抓)、Minimal Bridge State 原子持久化 |
160
- | unit — 低層 | 雙格式 catalog 解析(codex+claude、open 政策、unavailable entry)、git locator/source-key(fixed `default` selector)、contained path/symlink、collision、投影(exposure)、source-cache(store/hit/LRU/pin/flock)、git acquisition(mock executor) |
161
- | integration | 真 Pi 縫:extension 註冊 `resources_discover` → 投影 skillPaths(startup/reload 一致、trust flag 無關、被動不變異) |
162
- | E2E | `/codex-marketplace` 薄 Pi adapter:overview/help 輸出路由、corrupt 重置通知、reload 門控 |
163
-
164
- Fixtures: `tests/fixtures/synthetic/`, `tests/fixtures/pinned/`(captured `SamWang32191/codex-plugins@98e78ca` snapshot)、`tests/fixtures/adversarial/`。See `tests/acceptance/` for the matrix runner that enforces per-row gating (any row failure blocks publish).
165
-
166
- Run locally:
167
-
168
- ```bash
169
- npm run typecheck
170
- npm test # full matrix (unit + integration + E2E)
171
- npm run test:acceptance # acceptance matrix only
172
- ```
173
-
174
- ## Development
175
-
176
- ```bash
177
- npm install
178
- npm run typecheck
179
- npm test
180
- npm run test:acceptance
181
- ```
182
-
183
- ## Domain vocabulary
184
-
185
- Canonical terms are defined in [`CONTEXT.md`](./CONTEXT.md) — use them verbatim (Bridge Package vs Bridge Extension, Bridge State vs Effective State, Marketplace Source, Source Key, Validation Snapshot, Projected Skill, etc.).
186
-
187
- ## Changelog & Releases
188
-
189
- See [`CHANGELOG.md`](./CHANGELOG.md) and [GitHub Releases](../../releases). Version `0.1.0` is the first SemVer release; Git tags mirror npm versions (`v0.1.0` → `0.1.0`).
30
+ | `add <路徑\|網址>` | 註冊 marketplace(本機資料夾、GitHub 網址、`owner/repo` 簡寫、SSH 定位器) |
31
+ | `list [名稱]` | 列出 plugins(編號/所屬 marketplace/狀態,可帶名稱過濾) |
32
+ | `install <編號\|名稱>` | 裝到**當下最新**並自動啟用+reload |
33
+ | `update` | 全部已註冊 marketplace 重抓最新 |
34
+ | `disable <名稱>` / `enable <名稱>` | 停用/啟用 plugin |
35
+ | `remove <名稱>` | 移除單支 plugin |
36
+ | `forget <名稱>` | 移除整個 marketplace(含其全部安裝) |
37
+ | `help` | 子命令清單 |
38
+
39
+ ## Documentation
40
+
41
+ 完整文件結構化於 `docs/` 下(GitHub 檢視;npm 上以本 README 為入口):
42
+
43
+ | 文件 | 內容 |
44
+ |------|------|
45
+ | [docs/installation.md](./docs/installation.md) | 安裝/更新/移除:Pi Extension 各模式、Headless CLI、`-l` 語意、requirements |
46
+ | [docs/usage.md](./docs/usage.md) | `/codex-marketplace` 使用手冊:九子命令細節、語意鐵則、Pi 原生 autocomplete |
47
+ | [docs/cli.md](./docs/cli.md) | Headless Bridge CLI:執行方式、輸出與退出代碼契約、狀態生效時機 |
48
+ | [docs/private-repos.md](./docs/private-repos.md) | 私有 Git repo:Credentialed Acquisition(核准式取得)、SSH 定位器 |
49
+ | [docs/architecture.md](./docs/architecture.md) | 架構:Bridge State storage、支援矩陣、領域詞彙、ADR |
50
+ | [docs/development.md](./docs/development.md) | 開發:驗證矩陣(發版 gate)、版本化與發布流程 |
51
+
52
+ 領域術語(正典詞彙)見 [`CONTEXT.md`](./CONTEXT.md);發布紀錄見 [`CHANGELOG.md`](./CHANGELOG.md) [GitHub Releases](https://github.com/SamWang32191/pi-codex-marketplace/releases)。
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { register } from "node:module";
3
+
4
+ try {
5
+ register("./ts-resolver.mjs", import.meta.url);
6
+ } catch {
7
+ // Module registration unsupported or already active
8
+ }
9
+
10
+ const { runCli } = await import("../src/cli/index.js");
11
+ const code = await runCli(process.argv.slice(2), process);
12
+ process.exitCode = code;
@@ -0,0 +1,15 @@
1
+ export async function resolve(specifier, context, nextResolve) {
2
+ try {
3
+ return await nextResolve(specifier, context);
4
+ } catch (err) {
5
+ if (specifier.endsWith(".js")) {
6
+ const tsSpecifier = specifier.slice(0, -3) + ".ts";
7
+ try {
8
+ return await nextResolve(tsSpecifier, context);
9
+ } catch {
10
+ // Fall back to original error
11
+ }
12
+ }
13
+ throw err;
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-codex-marketplace",
3
- "version": "0.7.0",
4
- "description": "Bridge Package for Codex and Claude Marketplace compatibility in Pi — 極簡 /codex-marketplace 純文字指令(add/list/install/update/disable/enable/remove/forget)、單一 Global Bridge State、當下最新安裝與即時投影",
3
+ "version": "1.0.0",
4
+ "description": "Bridge Package for Codex and Claude Marketplace compatibility in Pi — 極簡 /codex-marketplace 純文字指令與 Headless CLI(add/list/install/update/disable/enable/remove/forget)、單一 Global Bridge State、當下最新安裝與即時投影",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",
@@ -20,6 +20,9 @@
20
20
  "engines": {
21
21
  "node": ">=22.19.0"
22
22
  },
23
+ "bin": {
24
+ "pi-codex-marketplace": "./bin/pi-codex-marketplace.js"
25
+ },
23
26
  "pi": {
24
27
  "extensions": [
25
28
  "./extensions/pi/index.ts"
@@ -56,6 +59,7 @@
56
59
  "prepublishOnly": "npm run typecheck && npm test"
57
60
  },
58
61
  "files": [
62
+ "bin",
59
63
  "extensions",
60
64
  "src",
61
65
  "README.md",
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import { randomUUID } from 'node:crypto';
9
- import { basename, isAbsolute, join, resolve } from 'node:path';
9
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
10
10
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
11
+ import { fileURLToPath } from 'node:url';
11
12
 
12
13
  import { parseFrontmatter } from '@earendil-works/pi-coding-agent';
13
14
 
@@ -59,6 +60,17 @@ export interface CommandResult {
59
60
  output: string;
60
61
  reload: boolean;
61
62
  stateReset?: boolean;
63
+ ok: boolean;
64
+ }
65
+
66
+ export function getPackageVersion(): string {
67
+ try {
68
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json');
69
+ const pkgJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
70
+ return pkgJson.version ?? '0.0.0';
71
+ } catch {
72
+ return '0.0.0';
73
+ }
62
74
  }
63
75
 
64
76
  const USAGE_LINE = '用法:/codex-marketplace <add|list|install|update|disable|enable|remove|forget|help>';
@@ -495,7 +507,7 @@ export async function runCommand(
495
507
  } catch (e) {
496
508
  const msg = e instanceof Error ? e.message : String(e);
497
509
  const messages = [`錯誤:讀取 Bridge State 失敗:${msg}`];
498
- return { messages, lines: messages, output: messages.join('\n\n'), reload: false, stateReset: false };
510
+ return { messages, lines: messages, output: messages.join('\n\n'), reload: false, stateReset: false, ok: false };
499
511
  }
500
512
 
501
513
  const messages: string[] = [];
@@ -513,6 +525,12 @@ export async function runCommand(
513
525
  const subargs = rawArgs.slice(1);
514
526
 
515
527
  switch (subcmd) {
528
+ case '--version':
529
+ case '-v':
530
+ case 'version': {
531
+ messages.push(getPackageVersion());
532
+ break;
533
+ }
516
534
  case 'help':
517
535
  case '-h':
518
536
  case '--help': {
@@ -732,13 +750,17 @@ export async function runCommand(
732
750
  const filteredRegs = state.registrations.filter(
733
751
  (r) => r.marketplaceName === filter || r.alias === filter || r.id === filter,
734
752
  );
735
- if (filteredRegs.length === 0 && state.registrations.length > 0) {
753
+ if (filteredRegs.length === 0) {
736
754
  messages.push(`找不到 marketplace "${filter}"`);
737
- // Still show all for discoverability
738
- messages.push(...formatOverview(state));
739
- const pluginLines = formatPluginListLines(state, undefined, opts);
740
- if (pluginLines.length > 0) messages.push(pluginLines.join('\n'));
741
- } else if (filteredRegs.length > 0) {
755
+ if (state.registrations.length > 0) {
756
+ // Still show all for discoverability
757
+ messages.push(...formatOverview(state));
758
+ const pluginLines = formatPluginListLines(state, undefined, opts);
759
+ if (pluginLines.length > 0) messages.push(pluginLines.join('\n'));
760
+ } else {
761
+ messages.push(USAGE_LINE);
762
+ }
763
+ } else {
742
764
  // Show filtered overview (reuse format but only filtered regs)
743
765
  const filteredState: MinimalBridgeState = {
744
766
  ...state,
@@ -748,10 +770,6 @@ export async function runCommand(
748
770
  messages.push(...formatOverview(filteredState));
749
771
  const pluginLines = formatPluginListLines(state, filter, opts);
750
772
  if (pluginLines.length > 0) messages.push(pluginLines.join('\n'));
751
- } else {
752
- // No registrations at all
753
- messages.push('尚無可列出的 plugin 或 marketplace。');
754
- messages.push(USAGE_LINE);
755
773
  }
756
774
  } else {
757
775
  if (state.registrations.length === 0 && state.installations.length === 0) {
@@ -1274,11 +1292,26 @@ export async function runCommand(
1274
1292
  }
1275
1293
  }
1276
1294
 
1295
+ const ok = !messages.some(
1296
+ (m) =>
1297
+ m.startsWith('錯誤:') ||
1298
+ m.startsWith('未知子命令') ||
1299
+ m.startsWith('用法:/codex-marketplace add') ||
1300
+ m.startsWith('用法:/codex-marketplace install') ||
1301
+ m.startsWith('用法:/codex-marketplace disable') ||
1302
+ m.startsWith('用法:/codex-marketplace enable') ||
1303
+ m.startsWith('用法:/codex-marketplace remove') ||
1304
+ m.startsWith('用法:/codex-marketplace forget') ||
1305
+ m.startsWith('已註冊過相同來源') ||
1306
+ m.startsWith('找不到 marketplace'),
1307
+ );
1308
+
1277
1309
  return {
1278
1310
  messages,
1279
1311
  lines: messages,
1280
1312
  output: messages.join('\n\n'),
1281
1313
  reload,
1282
1314
  stateReset: wasReset,
1315
+ ok,
1283
1316
  };
1284
1317
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CLI Adapter — Pure headless command-line entry seam for pi-codex-marketplace (#132, #133)
3
+ *
4
+ * Provides:
5
+ * - runCli(argv, io, opts): Pure adapter driving runCommand with stdout/stderr/exit code contract
6
+ * - formatCliOutput(result): Formats CommandResult for CLI output, replacing in-process reload notices
7
+ * - getPackageVersion(): Retrieves package version from package.json
8
+ */
9
+
10
+ import { runCommand, getPackageVersion, type CommandOptions, type CommandResult } from "../bridge/command.js";
11
+
12
+ export { getPackageVersion };
13
+
14
+ export interface CliIO {
15
+ stdout?: { write: (chunk: string) => unknown } | ((chunk: string) => unknown);
16
+ stderr?: { write: (chunk: string) => unknown } | ((chunk: string) => unknown);
17
+ exit?: (code: number) => unknown;
18
+ }
19
+
20
+ export const RELOAD_NOTICE = "已寫入 Bridge State · 下次 pi session//reload 生效";
21
+ export const RELOAD_PLACEHOLDER = "已重新載入生效";
22
+
23
+ export function formatCliOutput(result: CommandResult): string {
24
+ let text = result.output;
25
+ if (result.reload) {
26
+ if (text.includes(RELOAD_PLACEHOLDER)) {
27
+ text = text.replaceAll(RELOAD_PLACEHOLDER, RELOAD_NOTICE);
28
+ } else {
29
+ text = text.length > 0 ? `${text}\n\n${RELOAD_NOTICE}` : RELOAD_NOTICE;
30
+ }
31
+ }
32
+ return text;
33
+ }
34
+
35
+ function writeStream(
36
+ target: { write: (chunk: string) => unknown } | ((chunk: string) => unknown) | undefined,
37
+ message: string,
38
+ ): void {
39
+ if (!target) return;
40
+ const chunk = message.endsWith("\n") ? message : `${message}\n`;
41
+ if (typeof target === "function") {
42
+ target(chunk);
43
+ } else if (typeof target.write === "function") {
44
+ target.write(chunk);
45
+ }
46
+ }
47
+
48
+ export async function runCli(
49
+ argv: string[],
50
+ io: CliIO = process,
51
+ opts: CommandOptions = {},
52
+ ): Promise<number> {
53
+ const exitWith = (code: number): number => {
54
+ if (typeof io.exit === "function") {
55
+ io.exit(code);
56
+ }
57
+ return code;
58
+ };
59
+
60
+ try {
61
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version")) {
62
+ writeStream(io.stdout, getPackageVersion());
63
+ return exitWith(0);
64
+ }
65
+
66
+ const result = await runCommand(argv, opts);
67
+ const output = formatCliOutput(result);
68
+
69
+ if (result.ok) {
70
+ if (output) {
71
+ writeStream(io.stdout, output);
72
+ }
73
+ return exitWith(0);
74
+ } else {
75
+ if (output) {
76
+ writeStream(io.stderr, output);
77
+ }
78
+ return exitWith(1);
79
+ }
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ writeStream(io.stderr, `錯誤:${msg}`);
83
+ return exitWith(1);
84
+ }
85
+ }