pneuma-skills 1.4.1 → 1.6.2
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 +54 -15
- package/bin/pneuma.ts +54 -13
- package/core/__tests__/fixtures/test-mode/manifest.ts +20 -0
- package/core/__tests__/mode-loader-external.test.ts +36 -0
- package/core/__tests__/mode-resolver.test.ts +155 -0
- package/core/mode-loader.ts +93 -26
- package/core/mode-resolver.ts +235 -0
- package/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/assets/{EditorPanel-CfeaFr28.js → EditorPanel-MdL_JiTY.js} +17 -17
- package/dist/assets/Tableau10-B-NsZVaP.js +1 -0
- package/dist/assets/{TerminalPanel-mxIXJFij.js → TerminalPanel-CbFaJVv1.js} +1 -1
- package/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/dist/assets/ar-SA-G6X2FPQ2-DI1IvzZy.js +10 -0
- package/dist/assets/arc-D165-OAx.js +1 -0
- package/dist/assets/array-BKyUJesY.js +1 -0
- package/dist/assets/az-AZ-76LH7QW2-DYGh7wld.js +1 -0
- package/dist/assets/bg-BG-XCXSNQG7-BPdSa6MK.js +5 -0
- package/dist/assets/blockDiagram-38ab4fdb-CEu2OzeC.js +118 -0
- package/dist/assets/bn-BD-2XOGV67Q-DkIs-slB.js +5 -0
- package/dist/assets/c4Diagram-3d4e48cf-DoIsm8EF.js +10 -0
- package/dist/assets/ca-ES-6MX7JW3Y-L170HCw7.js +8 -0
- package/dist/assets/channel-D5iBNjbH.js +1 -0
- package/dist/assets/classDiagram-70f12bd4-QQ76MqW5.js +2 -0
- package/dist/assets/classDiagram-v2-f2320105-BmabMbok.js +2 -0
- package/dist/assets/clone-BEE8hBIa.js +1 -0
- package/dist/assets/createText-2e5e7dd3-tL3VdL-p.js +7 -0
- package/dist/assets/cs-CZ-2BRQDIVT-d9QyNfe3.js +11 -0
- package/dist/assets/da-DK-5WZEPLOC-C9h-0UH1.js +5 -0
- package/dist/assets/de-DE-XR44H4JA-0_J2pE7J.js +8 -0
- package/dist/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
- package/dist/assets/directory-open-4ed118d0-CunoC1EB.js +1 -0
- package/dist/assets/edges-e0da2a9e-9Gd7e5_q.js +4 -0
- package/dist/assets/el-GR-BZB4AONW-CPGoB07j.js +10 -0
- package/dist/assets/erDiagram-9861fffd--6rJdAlm.js +51 -0
- package/dist/assets/es-ES-U4NZUMDT-0MvEQWSD.js +9 -0
- package/dist/assets/eu-ES-A7QVB2H4-BRU3FvSm.js +11 -0
- package/dist/assets/extends-CF3RwP-h.js +1 -0
- package/dist/assets/fa-IR-HGAKTJCU-CnBKZHHO.js +8 -0
- package/dist/assets/fi-FI-Z5N7JZ37-BgG37SFV.js +6 -0
- package/dist/assets/file-open-002ab408-DIuFHtCF.js +1 -0
- package/dist/assets/file-open-7c801643-684qeFg4.js +1 -0
- package/dist/assets/file-save-3189631c-C1wFhQhH.js +1 -0
- package/dist/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
- package/dist/assets/flowDb-956e92f1-DLjSZLGX.js +10 -0
- package/dist/assets/flowDiagram-66a62f08-B3qKWVtA.js +4 -0
- package/dist/assets/flowDiagram-v2-96b9c2cf-DSrHZQLe.js +1 -0
- package/dist/assets/flowchart-elk-definition-4a651766-tTc1ftqg.js +139 -0
- package/dist/assets/fr-FR-RHASNOE6-Jl6lji35.js +9 -0
- package/dist/assets/ganttDiagram-c361ad54-lSzFV-3P.js +257 -0
- package/dist/assets/gitGraphDiagram-72cf32ee-7SNlQmFU.js +70 -0
- package/dist/assets/gl-ES-HMX3MZ6V-BFbSruD0.js +10 -0
- package/dist/assets/graph-BcpGRXRu.js +1 -0
- package/dist/assets/he-IL-6SHJWFNN-BNLVLLSm.js +10 -0
- package/dist/assets/hi-IN-IWLTKZ5I-Dsj3ZWPl.js +4 -0
- package/dist/assets/hu-HU-A5ZG7DT2-DCbDqW_4.js +7 -0
- package/dist/assets/id-ID-SAP4L64H-BmeTOocX.js +10 -0
- package/dist/assets/image-blob-reduce.esm-D6s-rqMO.js +7 -0
- package/dist/assets/index-3862675e-DIhdBfTo.js +1 -0
- package/dist/assets/index-BcAUh2Nm.css +1 -0
- package/dist/assets/index-DB9RcBPw.css +1 -0
- package/dist/assets/index-DE39xQEr.js +95 -0
- package/dist/assets/index-DcCNGeQt.js +1 -0
- package/dist/assets/index-qUAZT-v0.js +97 -0
- package/dist/assets/infoDiagram-f8f76790-GkJF5VfW.js +7 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/it-IT-JPQ66NNP-B7_H3wxP.js +11 -0
- package/dist/assets/ja-JP-DBVTYXUO-Kiyckl1D.js +8 -0
- package/dist/assets/journeyDiagram-49397b02-BgjYuH61.js +139 -0
- package/dist/assets/kaa-6HZHGXH3-C9JGzPVZ.js +1 -0
- package/dist/assets/kab-KAB-ZGHBKWFO-Byx6HzwL.js +8 -0
- package/dist/assets/katex-BlHpptmG.js +261 -0
- package/dist/assets/kk-KZ-P5N5QNE5-Cgk7qzOf.js +1 -0
- package/dist/assets/km-KH-HSX4SM5Z-Elll5unm.js +11 -0
- package/dist/assets/ko-KR-MTYHY66A-CsOtBxCZ.js +9 -0
- package/dist/assets/ku-TR-6OUDTVRD-CuTEXM4p.js +9 -0
- package/dist/assets/layout-CUWfUNS2.js +1 -0
- package/dist/assets/line-CVwrY-Vb.js +1 -0
- package/dist/assets/linear-BrME-eAl.js +1 -0
- package/dist/assets/lt-LT-XHIRWOB4-C8c0WkUW.js +3 -0
- package/dist/assets/lv-LV-5QDEKY6T-CEKRGmw8.js +7 -0
- package/dist/assets/{manifest-D0B-XRFy.js → manifest-BHamNdmX.js} +1 -1
- package/dist/assets/manifest-CjjXAwgC.js +10 -0
- package/dist/assets/mindmap-definition-fc14e90a-U0UAQ-Fw.js +425 -0
- package/dist/assets/mr-IN-CRQNXWMA-CTxbfZqj.js +13 -0
- package/dist/assets/my-MM-5M5IBNSE-DcDY6QTm.js +1 -0
- package/dist/assets/nb-NO-T6EIAALU-C6KHf4vB.js +10 -0
- package/dist/assets/nl-NL-IS3SIHDZ-BGmeQ17q.js +8 -0
- package/dist/assets/nn-NO-6E72VCQL-C-tmVDb4.js +8 -0
- package/dist/assets/oc-FR-POXYY2M6-C5fnow4b.js +8 -0
- package/dist/assets/ordinal-BENe2yWM.js +1 -0
- package/dist/assets/pa-IN-N4M65BXN-D6t69GoD.js +4 -0
- package/dist/assets/path-CbwjOpE9.js +1 -0
- package/dist/assets/percentages-BXMCSKIN-CuqegRFs.js +215 -0
- package/dist/assets/pica-DICzK7iG.js +7 -0
- package/dist/assets/pieDiagram-8a3498a8-CbbXLjm7.js +35 -0
- package/dist/assets/pl-PL-T2D74RX3-DER2H0wo.js +9 -0
- package/dist/assets/{pneuma-mode-DPKBeHJ5.js → pneuma-mode-D2DgSpg6.js} +6 -6
- package/dist/assets/pneuma-mode-DlMa-HY8.js +3 -0
- package/dist/assets/{pneuma-mode-Ru5wiX6u.js → pneuma-mode-Ogulzc3b.js} +1 -1
- package/dist/assets/pt-BR-5N22H2LF-CJHfbbIQ.js +9 -0
- package/dist/assets/pt-PT-UZXXM6DQ-CkCsXMDT.js +9 -0
- package/dist/assets/quadrantDiagram-120e2f19-D_mc357R.js +7 -0
- package/dist/assets/requirementDiagram-deff3bca-zCTbZYaF.js +52 -0
- package/dist/assets/ro-RO-JPDTUUEW-BSHMxikS.js +11 -0
- package/dist/assets/roundRect-0PYZxl1G.js +1 -0
- package/dist/assets/ru-RU-B4JR7IUQ-DhnnXCsj.js +9 -0
- package/dist/assets/sankeyDiagram-04a897e0-BvDjTJE5.js +8 -0
- package/dist/assets/sequenceDiagram-704730f1-DyDtB4bR.js +122 -0
- package/dist/assets/si-LK-N5RQ5JYF-CBpcfcVd.js +1 -0
- package/dist/assets/sk-SK-C5VTKIMK-AQrsUpWY.js +6 -0
- package/dist/assets/sl-SI-NN7IZMDC-CT83Tcto.js +6 -0
- package/dist/assets/stateDiagram-587899a1-BLA3XkRF.js +1 -0
- package/dist/assets/stateDiagram-v2-d93cdb3a-DR7IYUqf.js +1 -0
- package/dist/assets/styles-6aaf32cf-D2GttATi.js +207 -0
- package/dist/assets/styles-9a916d00-BpiIjJGB.js +160 -0
- package/dist/assets/styles-c10674c1-DvdNSnC7.js +116 -0
- package/dist/assets/subset-shared.chunk-B4kMbE2H.js +84 -0
- package/dist/assets/subset-worker.chunk-CPiIs6nq.js +1 -0
- package/dist/assets/sv-SE-XGPEYMSR-BoK_Plbz.js +10 -0
- package/dist/assets/svgDrawCommon-08f97a94-3WJM28cv.js +1 -0
- package/dist/assets/ta-IN-2NMHFXQM-d4L55p0Z.js +9 -0
- package/dist/assets/th-TH-HPSO5L25-BM_rrii7.js +2 -0
- package/dist/assets/timeline-definition-85554ec2-CyUtcC-f.js +61 -0
- package/dist/assets/tr-TR-DEFEU3FU-HX-ieOrB.js +7 -0
- package/dist/assets/uk-UA-QMV73CPH-BLoCnHH_.js +6 -0
- package/dist/assets/vi-VN-M7AON7JQ-25ijC9l1.js +5 -0
- package/dist/assets/xychartDiagram-e933f94c-BnuVVpgF.js +7 -0
- package/dist/assets/zh-CN-LNUGB5OW-C40B56rX.js +10 -0
- package/dist/assets/zh-HK-E62DVLB3-NkoZbsR_.js +1 -0
- package/dist/assets/zh-TW-RAJ6MFWO-DVkT3BzC.js +9 -0
- package/dist/index.html +2 -2
- package/modes/doc/manifest.ts +1 -1
- package/modes/doc/pneuma-mode.ts +1 -1
- package/modes/doc/seed/README.md +76 -0
- package/modes/draw/manifest.ts +56 -0
- package/modes/draw/pneuma-mode.ts +44 -0
- package/modes/draw/seed/drawing.excalidraw +526 -0
- package/modes/draw/skill/SKILL.md +201 -0
- package/modes/draw/viewer/DrawPreview.tsx +472 -0
- package/modes/slide/pneuma-mode.ts +1 -1
- package/modes/slide/skill/SKILL.md +21 -6
- package/modes/slide/skill/scripts/generate_image.mjs +389 -0
- package/modes/slide/{components → viewer}/SlideIframePool.tsx +9 -25
- package/package.json +2 -1
- package/server/__tests__/file-watcher.test.ts +123 -0
- package/server/__tests__/skill-installer.test.ts +282 -0
- package/server/__tests__/ws-bridge-browser.test.ts +464 -0
- package/server/__tests__/ws-bridge-controls.test.ts +205 -0
- package/server/__tests__/ws-bridge-replay.test.ts +284 -0
- package/server/index.ts +174 -40
- package/dist/assets/index-DC1enq6_.css +0 -1
- package/dist/assets/index-zOZ-kT8a.js +0 -95
- package/modes/slide/skill/scripts/generate_image.py +0 -333
- /package/modes/doc/{components → viewer}/DocPreview.tsx +0 -0
- /package/modes/slide/{components → viewer}/SlidePreview.tsx +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ ModeManifest(skill + viewer + agent_config) × AgentBackend × RuntimeShell
|
|
|
12
12
|
|
|
13
13
|
## Demo
|
|
14
14
|
|
|
15
|
-
Ships with **Doc Mode** (markdown editing)
|
|
15
|
+
Ships with **Doc Mode** (markdown editing), **Slide Mode** (presentation editing), and **Draw Mode** (Excalidraw whiteboard). Here's Doc Mode — Claude Code edits `.md` files and you see the rendered result in real-time:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
┌─────────────────────────────┬──────────────────────────┐
|
|
@@ -69,8 +69,12 @@ This will:
|
|
|
69
69
|
pneuma-skills <mode> [options]
|
|
70
70
|
|
|
71
71
|
Modes:
|
|
72
|
-
doc
|
|
73
|
-
slide
|
|
72
|
+
doc Markdown document editing mode
|
|
73
|
+
slide Presentation editing mode (HTML slides with iframe preview)
|
|
74
|
+
draw Excalidraw whiteboard drawing mode
|
|
75
|
+
/path/to/mode Load mode from a local directory
|
|
76
|
+
github:user/repo Load mode from a GitHub repository
|
|
77
|
+
github:user/repo#branch Load mode from a specific branch/tag
|
|
74
78
|
|
|
75
79
|
Options:
|
|
76
80
|
--workspace <path> Target workspace directory (default: current directory)
|
|
@@ -78,6 +82,29 @@ Options:
|
|
|
78
82
|
--no-open Don't auto-open the browser
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
### Remote / External Modes
|
|
86
|
+
|
|
87
|
+
Pneuma supports loading modes from outside the built-in `modes/` directory:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Load from a local directory (must contain manifest.ts and pneuma-mode.ts)
|
|
91
|
+
bunx pneuma-skills /path/to/my-custom-mode --workspace ~/project
|
|
92
|
+
|
|
93
|
+
# Load from a GitHub repository
|
|
94
|
+
bunx pneuma-skills github:pandazki/pneuma-mode-canvas --workspace ~/project
|
|
95
|
+
|
|
96
|
+
# Load from a specific branch or tag
|
|
97
|
+
bunx pneuma-skills github:pandazki/pneuma-mode-canvas#develop --workspace ~/project
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
GitHub repositories are cloned to `~/.pneuma/modes/` and cached locally. Subsequent runs will fetch the latest changes.
|
|
101
|
+
|
|
102
|
+
A mode package must contain:
|
|
103
|
+
- `manifest.ts` — default export of `ModeManifest`
|
|
104
|
+
- `pneuma-mode.ts` — default export of `ModeDefinition`
|
|
105
|
+
- `viewer/` — React preview components
|
|
106
|
+
- `skill/` — Skill files (optional)
|
|
107
|
+
|
|
81
108
|
## Architecture
|
|
82
109
|
|
|
83
110
|
Pneuma is organized in four layers, each with a clear contract boundary:
|
|
@@ -90,7 +117,7 @@ Pneuma is organized in four layers, each with a clear contract boundary:
|
|
|
90
117
|
├─────────────────────────────────────────────────────────┤
|
|
91
118
|
│ Layer 3: Content Viewer │
|
|
92
119
|
│ ViewerContract — "how to render, select, update" │
|
|
93
|
-
│ modes/doc/
|
|
120
|
+
│ modes/doc/viewer/DocPreview.tsx │
|
|
94
121
|
├─────────────────────────────────────────────────────────┤
|
|
95
122
|
│ Layer 2: Agent Bridge │
|
|
96
123
|
│ AgentBackend — "how to launch, communicate, lifecycle" │
|
|
@@ -112,11 +139,11 @@ When Claude Code edits files, chokidar detects the changes and pushes updated co
|
|
|
112
139
|
|
|
113
140
|
| Contract | Responsibility | Extend to... |
|
|
114
141
|
|----------|---------------|-------------|
|
|
115
|
-
| **ModeManifest** | Declares skill, viewer config, agent preferences, init seeds | Add new modes (
|
|
142
|
+
| **ModeManifest** | Declares skill, viewer config, agent preferences, init seeds | Add new modes (mindmap, canvas, etc.) |
|
|
116
143
|
| **ViewerContract** | Preview component, context extraction, update strategy | Custom renderers (iframe, D3, Monaco) |
|
|
117
144
|
| **AgentBackend** | Launch, resume, kill, capability declaration | Other agents (Codex, Aider) |
|
|
118
145
|
|
|
119
|
-
Contracts are defined in `core/types/` with
|
|
146
|
+
Contracts are defined in `core/types/` with 81 tests in `core/__tests__/`.
|
|
120
147
|
|
|
121
148
|
## Project Structure
|
|
122
149
|
|
|
@@ -130,19 +157,25 @@ pneuma-skills/
|
|
|
130
157
|
│ │ ├── agent-backend.ts # AgentBackend, AgentCapabilities
|
|
131
158
|
│ │ ├── mode-definition.ts # ModeDefinition (manifest + viewer)
|
|
132
159
|
│ │ └── index.ts # Re-exports
|
|
133
|
-
│ ├── mode-loader.ts # Dynamic mode discovery and loading
|
|
134
|
-
│
|
|
160
|
+
│ ├── mode-loader.ts # Dynamic mode discovery and loading (builtin + external)
|
|
161
|
+
│ ├── mode-resolver.ts # Mode source resolution (builtin/local/github)
|
|
162
|
+
│ └── __tests__/ # 81 tests
|
|
135
163
|
├── modes/
|
|
136
164
|
│ ├── doc/
|
|
137
165
|
│ │ ├── pneuma-mode.ts # Doc Mode definition (manifest + viewer)
|
|
138
166
|
│ │ ├── skill/SKILL.md # Skill prompt for Claude Code
|
|
139
|
-
│ │ └──
|
|
167
|
+
│ │ └── viewer/
|
|
140
168
|
│ │ └── DocPreview.tsx # Markdown preview with select/edit modes
|
|
141
|
-
│
|
|
142
|
-
│
|
|
143
|
-
│
|
|
144
|
-
│
|
|
145
|
-
│
|
|
169
|
+
│ ├── slide/
|
|
170
|
+
│ │ ├── pneuma-mode.ts # Slide Mode definition (manifest + viewer)
|
|
171
|
+
│ │ ├── skill/ # Skill package (SKILL.md + design docs + scripts)
|
|
172
|
+
│ │ └── viewer/
|
|
173
|
+
│ │ └── SlidePreview.tsx # Slide carousel with iframe preview
|
|
174
|
+
│ └── draw/
|
|
175
|
+
│ ├── pneuma-mode.ts # Draw Mode definition (manifest + viewer)
|
|
176
|
+
│ ├── skill/SKILL.md # Skill prompt for Claude Code
|
|
177
|
+
│ └── viewer/
|
|
178
|
+
│ └── DrawPreview.tsx # Excalidraw editor
|
|
146
179
|
├── backends/
|
|
147
180
|
│ └── claude-code/
|
|
148
181
|
│ ├── index.ts # ClaudeCodeBackend implements AgentBackend
|
|
@@ -167,6 +200,10 @@ pneuma-skills/
|
|
|
167
200
|
│ ├── ToolBlock.tsx # Expandable tool call cards
|
|
168
201
|
│ ├── PermissionBanner.tsx # Tool permission approval UI
|
|
169
202
|
│ └── TopBar.tsx # Tabs (Chat/Context/Terminal) + status
|
|
203
|
+
├── snapshot/ # Snapshot push/pull via Cloudflare R2
|
|
204
|
+
│ ├── push.ts # Pack and upload workspace
|
|
205
|
+
│ ├── pull.ts # Download and extract workspace
|
|
206
|
+
│ └── r2.ts # R2 storage client
|
|
170
207
|
└── docs/
|
|
171
208
|
├── adr/ # Architecture Decision Records (1-11)
|
|
172
209
|
├── design/ # Active design documents
|
|
@@ -185,6 +222,7 @@ pneuma-skills/
|
|
|
185
222
|
| Markdown | [react-markdown](https://github.com/remarkjs/react-markdown) + remark-gfm |
|
|
186
223
|
| Terminal | [xterm.js](https://xtermjs.org) + Bun native PTY |
|
|
187
224
|
| File Watching | [chokidar](https://github.com/paulmillr/chokidar) 4 |
|
|
225
|
+
| Drawing | [Excalidraw](https://excalidraw.com) 0.18 |
|
|
188
226
|
| Agent | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) via `--sdk-url` |
|
|
189
227
|
|
|
190
228
|
## Features
|
|
@@ -205,11 +243,12 @@ pneuma-skills/
|
|
|
205
243
|
|
|
206
244
|
- [x] Doc Mode — Markdown WYSIWYG editing
|
|
207
245
|
- [x] Slide Mode — Presentation editing with iframe preview, drag-reorder, AI image generation
|
|
246
|
+
- [x] Draw Mode — Excalidraw whiteboard with `.excalidraw` file editing
|
|
208
247
|
- [x] Element selection & inline editing
|
|
209
248
|
- [x] Session persistence & resume
|
|
210
249
|
- [x] Terminal, tasks, context panel
|
|
211
250
|
- [x] v1.0 contract architecture (ModeManifest, ViewerContract, AgentBackend)
|
|
212
|
-
- [
|
|
251
|
+
- [x] Remote mode loading — `pneuma github:user/repo` or local path (v1.x)
|
|
213
252
|
- [ ] Additional agent backends — Codex CLI, custom agents (v1.x)
|
|
214
253
|
|
|
215
254
|
## Acknowledgements
|
package/bin/pneuma.ts
CHANGED
|
@@ -15,9 +15,11 @@ import { startServer } from "../server/index.js";
|
|
|
15
15
|
import { ClaudeCodeBackend } from "../backends/claude-code/index.js";
|
|
16
16
|
import { installSkill } from "../server/skill-installer.js";
|
|
17
17
|
import { startFileWatcher } from "../server/file-watcher.js";
|
|
18
|
-
import { loadModeManifest,
|
|
18
|
+
import { loadModeManifest, listBuiltinModes, registerExternalMode } from "../core/mode-loader.js";
|
|
19
19
|
import type { ModeManifest } from "../core/types/mode-manifest.js";
|
|
20
20
|
import { applyTemplateParams } from "../server/skill-installer.js";
|
|
21
|
+
import { resolveMode as resolveModeSource, isExternalMode } from "../core/mode-resolver.js";
|
|
22
|
+
import type { ResolvedMode } from "../core/mode-resolver.js";
|
|
21
23
|
|
|
22
24
|
const PROJECT_ROOT = resolve(dirname(import.meta.path), "..");
|
|
23
25
|
|
|
@@ -179,20 +181,40 @@ async function main() {
|
|
|
179
181
|
|
|
180
182
|
const { mode, workspace, port, noOpen } = parseArgs(process.argv);
|
|
181
183
|
|
|
182
|
-
// Validate mode
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
// Validate mode — support builtin names, local paths, and github: specifiers
|
|
185
|
+
if (!mode) {
|
|
186
|
+
const modeList = listBuiltinModes().join("|");
|
|
187
|
+
console.log(
|
|
188
|
+
`Usage: pneuma <${modeList}|/path/to/mode|github:user/repo> [--workspace <path>] [--port <number>] [--no-open]`,
|
|
189
|
+
);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Resolve mode source (builtin, local path, or github clone)
|
|
194
|
+
let resolved: ResolvedMode;
|
|
195
|
+
try {
|
|
196
|
+
resolved = await resolveModeSource(mode, PROJECT_ROOT);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
199
|
+
console.error(`[pneuma] Failed to resolve mode "${mode}": ${msg}`);
|
|
187
200
|
process.exit(1);
|
|
188
201
|
}
|
|
189
202
|
|
|
203
|
+
// For external modes, register them in the mode-loader before loading
|
|
204
|
+
if (resolved.type !== "builtin") {
|
|
205
|
+
registerExternalMode(resolved.name, resolved.path);
|
|
206
|
+
console.log(`[pneuma] External mode "${resolved.name}" loaded from ${resolved.path}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
// Load mode manifest (no React deps — backend safe)
|
|
210
|
+
// Use resolved.name for lookup since external modes are registered under that name
|
|
211
|
+
const modeName = resolved.name;
|
|
191
212
|
let manifest: ModeManifest;
|
|
192
213
|
try {
|
|
193
|
-
manifest = await loadModeManifest(
|
|
214
|
+
manifest = await loadModeManifest(modeName);
|
|
194
215
|
} catch (err) {
|
|
195
|
-
|
|
216
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
217
|
+
console.error(`[pneuma] Failed to load mode "${modeName}": ${msg}`);
|
|
196
218
|
process.exit(1);
|
|
197
219
|
}
|
|
198
220
|
|
|
@@ -206,7 +228,7 @@ async function main() {
|
|
|
206
228
|
console.log(`[pneuma] Created workspace: ${workspace}`);
|
|
207
229
|
}
|
|
208
230
|
|
|
209
|
-
console.log(`[pneuma] Mode: ${manifest.displayName} (${
|
|
231
|
+
console.log(`[pneuma] Mode: ${manifest.displayName} (${modeName})`);
|
|
210
232
|
console.log(`[pneuma] Workspace: ${workspace}`);
|
|
211
233
|
|
|
212
234
|
// 0.5 Resolve init params (interactive on first run, then cached)
|
|
@@ -228,7 +250,8 @@ async function main() {
|
|
|
228
250
|
}
|
|
229
251
|
|
|
230
252
|
// 1. Install skill + inject CLAUDE.md (driven by manifest)
|
|
231
|
-
|
|
253
|
+
// Use resolved path for external modes, PROJECT_ROOT/modes/{name} for builtin
|
|
254
|
+
const modeSourceDir = resolved.path;
|
|
232
255
|
const skillTarget = join(workspace, ".claude", "skills", manifest.skill.installName);
|
|
233
256
|
let skipSkillInstall = false;
|
|
234
257
|
|
|
@@ -263,8 +286,11 @@ async function main() {
|
|
|
263
286
|
|
|
264
287
|
if (!hasContent && manifest.init.seedFiles) {
|
|
265
288
|
const hasParams = Object.keys(resolvedParams).length > 0;
|
|
289
|
+
// For builtin modes, seed paths are relative to PROJECT_ROOT
|
|
290
|
+
// For external modes, seed paths are relative to the mode package directory
|
|
291
|
+
const seedBase = resolved.type === "builtin" ? PROJECT_ROOT : resolved.path;
|
|
266
292
|
for (const [src, dst] of Object.entries(manifest.init.seedFiles)) {
|
|
267
|
-
const srcPath = join(
|
|
293
|
+
const srcPath = join(seedBase, src);
|
|
268
294
|
if (existsSync(srcPath)) {
|
|
269
295
|
const dstPath = join(workspace, dst);
|
|
270
296
|
mkdirSync(dirname(dstPath), { recursive: true });
|
|
@@ -302,6 +328,10 @@ async function main() {
|
|
|
302
328
|
watchPatterns: manifest.viewer.watchPatterns,
|
|
303
329
|
...(isDev ? {} : { distDir }),
|
|
304
330
|
...(Object.keys(resolvedParams).length > 0 ? { initParams: resolvedParams } : {}),
|
|
331
|
+
// Pass external mode info for the /api/mode-info endpoint
|
|
332
|
+
...(resolved.type !== "builtin"
|
|
333
|
+
? { externalMode: { name: resolved.name, path: resolved.path, type: resolved.type } }
|
|
334
|
+
: {}),
|
|
305
335
|
});
|
|
306
336
|
|
|
307
337
|
// 4. Launch Agent backend (driven by manifest)
|
|
@@ -343,7 +373,7 @@ async function main() {
|
|
|
343
373
|
saveSession(workspace, {
|
|
344
374
|
sessionId: session.sessionId,
|
|
345
375
|
agentSessionId: existing?.agentSessionId,
|
|
346
|
-
mode,
|
|
376
|
+
mode: modeName,
|
|
347
377
|
createdAt: existing?.createdAt || Date.now(),
|
|
348
378
|
});
|
|
349
379
|
|
|
@@ -413,12 +443,23 @@ async function main() {
|
|
|
413
443
|
// Dev mode: start Vite dev server
|
|
414
444
|
const VITE_PORT = 17996;
|
|
415
445
|
console.log(`[pneuma] Starting Vite dev server on port ${VITE_PORT}...`);
|
|
446
|
+
|
|
447
|
+
// Pass external mode path to Vite via env var (for server.fs.allow)
|
|
448
|
+
const viteEnv: Record<string, string> = {
|
|
449
|
+
...process.env as Record<string, string>,
|
|
450
|
+
};
|
|
451
|
+
if (resolved.type !== "builtin") {
|
|
452
|
+
viteEnv.PNEUMA_EXTERNAL_MODE_PATH = resolved.path;
|
|
453
|
+
viteEnv.PNEUMA_EXTERNAL_MODE_NAME = resolved.name;
|
|
454
|
+
}
|
|
455
|
+
|
|
416
456
|
viteProc = Bun.spawn(
|
|
417
457
|
["bunx", "vite", "--port", String(VITE_PORT), "--strictPort"],
|
|
418
458
|
{
|
|
419
459
|
cwd: PROJECT_ROOT,
|
|
420
460
|
stdout: "pipe",
|
|
421
461
|
stderr: "pipe",
|
|
462
|
+
env: viteEnv,
|
|
422
463
|
}
|
|
423
464
|
);
|
|
424
465
|
|
|
@@ -443,7 +484,7 @@ async function main() {
|
|
|
443
484
|
|
|
444
485
|
// 7. Open browser (include mode in URL for frontend)
|
|
445
486
|
if (!noOpen) {
|
|
446
|
-
const url = `http://localhost:${browserPort}?session=${session.sessionId}&mode=${
|
|
487
|
+
const url = `http://localhost:${browserPort}?session=${session.sessionId}&mode=${modeName}`;
|
|
447
488
|
console.log(`[pneuma] Opening browser: ${url}`);
|
|
448
489
|
try {
|
|
449
490
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test mode manifest — used by mode-resolver integration tests.
|
|
3
|
+
*/
|
|
4
|
+
const testManifest = {
|
|
5
|
+
name: "test-mode",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
displayName: "Test Mode",
|
|
8
|
+
description: "A minimal mode for testing",
|
|
9
|
+
skill: {
|
|
10
|
+
sourceDir: "skill",
|
|
11
|
+
installName: "pneuma-test",
|
|
12
|
+
claudeMdSection: "## Test Mode",
|
|
13
|
+
},
|
|
14
|
+
viewer: {
|
|
15
|
+
watchPatterns: ["**/*.txt"],
|
|
16
|
+
ignorePatterns: ["node_modules/**"],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default testManifest;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode Loader — External mode registration tests
|
|
3
|
+
*
|
|
4
|
+
* 验证 registerExternalMode 和 loadModeManifest 对外部 mode 的支持。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect } from "bun:test";
|
|
8
|
+
import { resolve, dirname } from "node:path";
|
|
9
|
+
import { registerExternalMode, loadModeManifest, listModes, listBuiltinModes } from "../mode-loader.js";
|
|
10
|
+
|
|
11
|
+
const TEST_MODE_PATH = resolve(dirname(import.meta.path), "fixtures/test-mode");
|
|
12
|
+
|
|
13
|
+
describe("registerExternalMode", () => {
|
|
14
|
+
test("registered external mode appears in listModes()", () => {
|
|
15
|
+
registerExternalMode("test-mode", TEST_MODE_PATH);
|
|
16
|
+
const modes = listModes();
|
|
17
|
+
expect(modes).toContain("test-mode");
|
|
18
|
+
expect(modes).toContain("doc");
|
|
19
|
+
expect(modes).toContain("slide");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("listBuiltinModes() does not include external modes", () => {
|
|
23
|
+
const builtins = listBuiltinModes();
|
|
24
|
+
expect(builtins).toContain("doc");
|
|
25
|
+
expect(builtins).toContain("slide");
|
|
26
|
+
expect(builtins).not.toContain("test-mode");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("loadModeManifest() works for registered external mode", async () => {
|
|
30
|
+
registerExternalMode("test-mode", TEST_MODE_PATH);
|
|
31
|
+
const manifest = await loadModeManifest("test-mode");
|
|
32
|
+
expect(manifest.name).toBe("test-mode");
|
|
33
|
+
expect(manifest.version).toBe("0.1.0");
|
|
34
|
+
expect(manifest.displayName).toBe("Test Mode");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode Resolver 测试
|
|
3
|
+
*
|
|
4
|
+
* 验证 mode 来源解析:
|
|
5
|
+
* - builtin mode 名称识别
|
|
6
|
+
* - local path 解析 (绝对路径、相对路径、~ 展开)
|
|
7
|
+
* - github 格式解析
|
|
8
|
+
* - 错误处理
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect } from "bun:test";
|
|
12
|
+
import { parseModeSpecifier, isExternalMode, resolveMode } from "../mode-resolver.js";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { resolve, dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
describe("parseModeSpecifier", () => {
|
|
17
|
+
// ── Builtin modes ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
test("recognizes builtin mode: doc", () => {
|
|
20
|
+
const result = parseModeSpecifier("doc");
|
|
21
|
+
expect(result.type).toBe("builtin");
|
|
22
|
+
expect(result.name).toBe("doc");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("recognizes builtin mode: slide", () => {
|
|
26
|
+
const result = parseModeSpecifier("slide");
|
|
27
|
+
expect(result.type).toBe("builtin");
|
|
28
|
+
expect(result.name).toBe("slide");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("unknown plain name falls back to builtin (let mode-loader handle it)", () => {
|
|
32
|
+
const result = parseModeSpecifier("mindmap");
|
|
33
|
+
expect(result.type).toBe("builtin");
|
|
34
|
+
expect(result.name).toBe("mindmap");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── Local paths ────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
test("parses absolute path as local mode", () => {
|
|
40
|
+
const result = parseModeSpecifier("/home/user/my-mode");
|
|
41
|
+
expect(result.type).toBe("local");
|
|
42
|
+
expect(result.name).toBe("my-mode");
|
|
43
|
+
expect(result.localPath).toBe("/home/user/my-mode");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("parses relative path with ./ as local mode", () => {
|
|
47
|
+
const result = parseModeSpecifier("./modes/custom");
|
|
48
|
+
expect(result.type).toBe("local");
|
|
49
|
+
expect(result.name).toBe("custom");
|
|
50
|
+
expect(result.localPath).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("parses relative path with ../ as local mode", () => {
|
|
54
|
+
const result = parseModeSpecifier("../other-project/my-mode");
|
|
55
|
+
expect(result.type).toBe("local");
|
|
56
|
+
expect(result.name).toBe("my-mode");
|
|
57
|
+
expect(result.localPath).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("expands ~ to home directory", () => {
|
|
61
|
+
const result = parseModeSpecifier("~/my-modes/custom");
|
|
62
|
+
expect(result.type).toBe("local");
|
|
63
|
+
expect(result.name).toBe("custom");
|
|
64
|
+
expect(result.localPath).toStartWith(homedir());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── GitHub specifiers ──────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
test("parses github:user/repo", () => {
|
|
70
|
+
const result = parseModeSpecifier("github:pandazki/pneuma-mode-canvas");
|
|
71
|
+
expect(result.type).toBe("github");
|
|
72
|
+
expect(result.name).toBe("pandazki-pneuma-mode-canvas");
|
|
73
|
+
expect(result.github).toEqual({
|
|
74
|
+
user: "pandazki",
|
|
75
|
+
repo: "pneuma-mode-canvas",
|
|
76
|
+
ref: "main",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("parses github:user/repo#branch", () => {
|
|
81
|
+
const result = parseModeSpecifier("github:pandazki/my-mode#develop");
|
|
82
|
+
expect(result.type).toBe("github");
|
|
83
|
+
expect(result.name).toBe("pandazki-my-mode");
|
|
84
|
+
expect(result.github).toEqual({
|
|
85
|
+
user: "pandazki",
|
|
86
|
+
repo: "my-mode",
|
|
87
|
+
ref: "develop",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("parses github:user/repo#tag", () => {
|
|
92
|
+
const result = parseModeSpecifier("github:user/repo#v1.0.0");
|
|
93
|
+
expect(result.type).toBe("github");
|
|
94
|
+
expect(result.github?.ref).toBe("v1.0.0");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("throws for invalid github specifier (no repo)", () => {
|
|
98
|
+
expect(() => parseModeSpecifier("github:user")).toThrow("Invalid GitHub mode specifier");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("throws for invalid github specifier (empty user)", () => {
|
|
102
|
+
expect(() => parseModeSpecifier("github:/repo")).toThrow("Invalid GitHub mode specifier");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── resolveMode integration tests ─────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const PROJECT_ROOT = resolve(dirname(import.meta.path), "../..");
|
|
109
|
+
|
|
110
|
+
describe("resolveMode", () => {
|
|
111
|
+
test("resolves builtin mode to modes/ directory", async () => {
|
|
112
|
+
const result = await resolveMode("doc", PROJECT_ROOT);
|
|
113
|
+
expect(result.type).toBe("builtin");
|
|
114
|
+
expect(result.name).toBe("doc");
|
|
115
|
+
expect(result.path).toBe(join(PROJECT_ROOT, "modes", "doc"));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("resolves local path to absolute directory", async () => {
|
|
119
|
+
const testModePath = resolve(dirname(import.meta.path), "fixtures/test-mode");
|
|
120
|
+
const result = await resolveMode(testModePath, PROJECT_ROOT);
|
|
121
|
+
expect(result.type).toBe("local");
|
|
122
|
+
expect(result.name).toBe("test-mode");
|
|
123
|
+
expect(result.path).toBe(testModePath);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("throws for non-existent local path", async () => {
|
|
127
|
+
await expect(resolveMode("/nonexistent/path/to/mode", PROJECT_ROOT)).rejects.toThrow(
|
|
128
|
+
"Local mode directory not found",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("throws for local path without manifest", async () => {
|
|
133
|
+
// Use a directory that exists but has no manifest.ts
|
|
134
|
+
const tmpDir = resolve(dirname(import.meta.path), "fixtures");
|
|
135
|
+
await expect(resolveMode(tmpDir, PROJECT_ROOT)).rejects.toThrow(
|
|
136
|
+
"missing manifest.ts",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("isExternalMode", () => {
|
|
142
|
+
test("builtin modes are not external", () => {
|
|
143
|
+
expect(isExternalMode("doc")).toBe(false);
|
|
144
|
+
expect(isExternalMode("slide")).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("local paths are external", () => {
|
|
148
|
+
expect(isExternalMode("/path/to/mode")).toBe(true);
|
|
149
|
+
expect(isExternalMode("./my-mode")).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("github specifiers are external", () => {
|
|
153
|
+
expect(isExternalMode("github:user/repo")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
package/core/mode-loader.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mode Loader — 解析、安装、加载 Mode。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 支持三种来源:
|
|
5
|
+
* - builtin: 内置 mode,从 modes/ 目录动态 import
|
|
6
|
+
* - local: 本地文件系统路径
|
|
7
|
+
* - github: GitHub 仓库 (通过 mode-resolver 克隆到本地缓存)
|
|
6
8
|
*
|
|
7
9
|
* 核心流程: resolveMode → ensureInstalled → loadFromSource
|
|
8
10
|
*/
|
|
@@ -13,15 +15,21 @@ import type { ModeDefinition } from "./types/mode-definition.js";
|
|
|
13
15
|
/**
|
|
14
16
|
* Mode 来源类型:
|
|
15
17
|
* - "builtin" — 内置 mode,从 modes/ 目录动态 import
|
|
18
|
+
* - "external" — 外部 mode,从绝对路径动态 import (local path 或 github clone)
|
|
16
19
|
*/
|
|
17
|
-
type ModeSource =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
type ModeSource =
|
|
21
|
+
| {
|
|
22
|
+
type: "builtin";
|
|
23
|
+
manifestLoader: () => Promise<ModeManifest>;
|
|
24
|
+
definitionLoader: () => Promise<ModeDefinition>;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
type: "external";
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
manifestLoader: () => Promise<ModeManifest>;
|
|
31
|
+
definitionLoader: () => Promise<ModeDefinition>;
|
|
32
|
+
};
|
|
25
33
|
|
|
26
34
|
/** 内置 mode 注册表 — 全部使用动态 import */
|
|
27
35
|
const builtinModes: Record<string, ModeSource> = {
|
|
@@ -39,8 +47,18 @@ const builtinModes: Record<string, ModeSource> = {
|
|
|
39
47
|
definitionLoader: () =>
|
|
40
48
|
import("../modes/slide/pneuma-mode.js").then((m) => m.default),
|
|
41
49
|
},
|
|
50
|
+
draw: {
|
|
51
|
+
type: "builtin",
|
|
52
|
+
manifestLoader: () =>
|
|
53
|
+
import("../modes/draw/manifest.js").then((m) => m.default),
|
|
54
|
+
definitionLoader: () =>
|
|
55
|
+
import("../modes/draw/pneuma-mode.js").then((m) => m.default),
|
|
56
|
+
},
|
|
42
57
|
};
|
|
43
58
|
|
|
59
|
+
/** 外部 mode 注册表 — 由 CLI 在启动时通过 registerExternalMode 注册 */
|
|
60
|
+
const externalModes: Record<string, ModeSource> = {};
|
|
61
|
+
|
|
44
62
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
45
63
|
|
|
46
64
|
/**
|
|
@@ -64,35 +82,84 @@ export async function loadModeManifest(name: string): Promise<ModeManifest> {
|
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
/**
|
|
67
|
-
* 列出所有已注册的 mode
|
|
85
|
+
* 列出所有已注册的 mode 名称 (包括 builtin 和已注册的 external)。
|
|
68
86
|
*/
|
|
69
87
|
export function listModes(): string[] {
|
|
88
|
+
return [...Object.keys(builtinModes), ...Object.keys(externalModes)];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 列出内置 mode 名称。
|
|
93
|
+
*/
|
|
94
|
+
export function listBuiltinModes(): string[] {
|
|
70
95
|
return Object.keys(builtinModes);
|
|
71
96
|
}
|
|
72
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 注册外部 mode (由 CLI 在启动时调用)。
|
|
100
|
+
*
|
|
101
|
+
* Backend context (Bun): 直接用 import() 加载绝对路径。
|
|
102
|
+
* Frontend context (browser/Vite): 用 /@fs/ URL 加载。
|
|
103
|
+
*
|
|
104
|
+
* @param name — Mode 名称 (用于注册和查找)
|
|
105
|
+
* @param absPath — Mode 包的绝对路径
|
|
106
|
+
*/
|
|
107
|
+
export function registerExternalMode(name: string, absPath: string): void {
|
|
108
|
+
const isBrowser = typeof window !== "undefined";
|
|
109
|
+
|
|
110
|
+
if (isBrowser) {
|
|
111
|
+
// Frontend: use Vite's /@fs/ URL scheme for dev mode
|
|
112
|
+
externalModes[name] = {
|
|
113
|
+
type: "external",
|
|
114
|
+
name,
|
|
115
|
+
path: absPath,
|
|
116
|
+
manifestLoader: () =>
|
|
117
|
+
import(/* @vite-ignore */ `/@fs${absPath}/manifest.ts`).then(
|
|
118
|
+
(m) => m.default,
|
|
119
|
+
),
|
|
120
|
+
definitionLoader: () =>
|
|
121
|
+
import(/* @vite-ignore */ `/@fs${absPath}/pneuma-mode.ts`).then(
|
|
122
|
+
(m) => m.default,
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
// Backend (Bun): use direct absolute path import
|
|
127
|
+
externalModes[name] = {
|
|
128
|
+
type: "external",
|
|
129
|
+
name,
|
|
130
|
+
path: absPath,
|
|
131
|
+
manifestLoader: () =>
|
|
132
|
+
import(/* @vite-ignore */ absPath + "/manifest.ts").then((m) => m.default),
|
|
133
|
+
definitionLoader: () =>
|
|
134
|
+
import(/* @vite-ignore */ absPath + "/pneuma-mode.ts").then((m) => m.default),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
73
139
|
// ── Internal ─────────────────────────────────────────────────────────────────
|
|
74
140
|
|
|
75
|
-
/** 解析 mode 来源 (
|
|
141
|
+
/** 解析 mode 来源 (查 builtin 和 external 注册表) */
|
|
76
142
|
function resolveMode(name: string): ModeSource {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
143
|
+
// Check external modes first (allows overriding builtin names)
|
|
144
|
+
const external = externalModes[name];
|
|
145
|
+
if (external) return external;
|
|
146
|
+
|
|
147
|
+
const builtin = builtinModes[name];
|
|
148
|
+
if (builtin) return builtin;
|
|
149
|
+
|
|
150
|
+
const available = listModes();
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Unknown mode: "${name}". Available: ${available.join(", ")}`,
|
|
153
|
+
);
|
|
83
154
|
}
|
|
84
155
|
|
|
85
|
-
/** 确保 mode 已安装 (
|
|
156
|
+
/** 确保 mode 已安装 (builtin 直接跳过,external 已由 mode-resolver 处理) */
|
|
86
157
|
async function ensureInstalled(_source: ModeSource): Promise<void> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// 不存在 → 从 source.url 拉取/安装
|
|
90
|
-
// 非信任来源 → 用户确认
|
|
158
|
+
// Both builtin and external modes are already resolved to local paths
|
|
159
|
+
return;
|
|
91
160
|
}
|
|
92
161
|
|
|
93
162
|
/** 从已安装的 source 加载完整 ModeDefinition */
|
|
94
163
|
async function loadDefinition(source: ModeSource): Promise<ModeDefinition> {
|
|
95
|
-
|
|
96
|
-
// v1.x: dynamic import from .pneuma/modes/{name}/pneuma-mode.js
|
|
97
|
-
throw new Error("Non-builtin mode loading not yet implemented");
|
|
164
|
+
return source.definitionLoader();
|
|
98
165
|
}
|