pm-canvas-viewer 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 path
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # pm-canvas-viewer
2
+
3
+ 本地画板查看器 + 文档编辑工具。一键启动,浏览器操作。
4
+
5
+ 配合 [pm-canvas skill](../skill/) 使用:Agent 生成画板文件,人用 viewer 预览画板并编辑关联文档。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm i -g pm-canvas-viewer
11
+ ```
12
+
13
+ 装好后全局命令 `canvas` 即可用(包内已含预构建前端,无需自行构建)。
14
+
15
+ ## 使用
16
+
17
+ ```bash
18
+ canvas open ./我的画板/
19
+ ```
20
+
21
+ 浏览器自动打开,左侧画板预览,右侧文档编辑面板。
22
+
23
+ ### 画板目录结构
24
+
25
+ ```
26
+ 我的画板/
27
+ ├── index.html ← 画板 shell(Agent 生成)
28
+ ├── canvas.json ← 画板元数据(Agent 生成)
29
+ ├── rows/ ← 画板行文件(Agent 生成)
30
+ │ ├── 01-home.html
31
+ │ └── 02-profile.html
32
+ └── docs/ ← 关联文档(人维护,viewer 读写)
33
+ ├── 需求规格.md
34
+ └── 业务流程.excalidraw
35
+ ```
36
+
37
+ ### 命令参数
38
+
39
+ ```bash
40
+ canvas open [目录] [选项]
41
+
42
+ # 选项
43
+ -p, --port <端口> 指定端口(默认 4800)
44
+ -V, --version 查看版本
45
+ -h, --help 查看帮助
46
+ ```
47
+
48
+ ## 功能
49
+
50
+ ### 画板预览(左侧)
51
+ - iframe 加载画板 index.html,所有 row 平铺展示
52
+
53
+ ### 文档面板(右侧,可收起)
54
+
55
+ **Markdown 编辑器**
56
+ - Tiptap 3 所见即所得编辑器(WYSIWYG,`@tiptap/markdown` 读写 MD)
57
+ - 工具栏:标题、加粗、斜体、列表、代码、表格可视化编辑
58
+ - `⌘B` 加粗、`⌘I` 斜体
59
+ - `⌘S` 保存到本地文件
60
+
61
+ **Excalidraw 编辑器**
62
+ - 完整 Excalidraw 编辑器(画流程图、架构图等)
63
+ - `⌘S` 保存为 .excalidraw 文件
64
+
65
+ **文件管理**
66
+ - 标签页切换不同文档
67
+ - `+` 按钮新建 Markdown 或 Excalidraw 文件
68
+ - 文件保存在画板目录的 `docs/` 下
69
+
70
+ ## 技术栈
71
+
72
+ | 层 | 技术 |
73
+ |---|---|
74
+ | CLI | Commander.js |
75
+ | 服务端 | Express(静态文件 + 文件读写 API) |
76
+ | 前端框架 | React 18 + Vite |
77
+ | MD 编辑器 | Tiptap 3 WYSIWYG(@tiptap/markdown) |
78
+ | 流程图编辑 | @excalidraw/excalidraw |
79
+ | 构建 | Vite 5 |
80
+
81
+ ## 开发
82
+
83
+ ```bash
84
+ # 启动开发服务器(前后端同时启动)
85
+ npm run dev
86
+
87
+ # 仅构建前端
88
+ npm run build
89
+
90
+ # 用开发版直接测试
91
+ node bin/cli.js open /path/to/canvas --port 4800
92
+ ```
93
+
94
+ ### 项目结构
95
+
96
+ ```
97
+ pm-canvas-viewer/
98
+ ├── bin/cli.js ← CLI 入口
99
+ ├── server/
100
+ │ ├── index.js ← Express 服务器
101
+ │ └── api/files.js ← 文件 CRUD API(限定 docs/ 目录)
102
+ ├── frontend/
103
+ │ ├── src/
104
+ │ │ ├── App.jsx ← 主布局
105
+ │ │ ├── CanvasView/ ← 画板 iframe
106
+ │ │ ├── DocPanel/ ← 右侧面板 + 标签页 + 新建
107
+ │ │ ├── MdEditor/ ← Tiptap MD 编辑器
108
+ │ │ └── ExcalidrawEditor/ ← Excalidraw 编辑器
109
+ │ └── vite.config.js
110
+ ├── dist/ ← 前端构建产物(gitignored)
111
+ └── ai/ ← AI 协作产物
112
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { resolve } from 'path';
4
+ import { existsSync } from 'fs';
5
+ import open from 'open';
6
+ import { createServer } from '../server/index.js';
7
+ import { scanForCanvases, tryReadCanvasMeta } from '../lib/scanner.js';
8
+ import { recordOpen, getRecentCanvases } from '../lib/history.js';
9
+ import { findAvailablePort, isPortFree, getPortOccupant, killPort, listRunningInstances, registerInstance } from '../lib/port.js';
10
+ import { fuzzySelectCanvas, fuzzySelectFromHistory, handlePortConflict } from '../lib/prompts.js';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('canvas')
16
+ .description('Local canvas viewer + doc editor')
17
+ .version('0.1.0');
18
+
19
+ // Default command: canvas — interactive fuzzy search over history
20
+ program
21
+ .action(async () => {
22
+ const entries = await getRecentCanvases();
23
+ const selected = await fuzzySelectFromHistory(entries);
24
+ if (!selected) process.exit(0);
25
+
26
+ const meta = await tryReadCanvasMeta(selected.path);
27
+ if (!meta) {
28
+ console.error(` Canvas no longer exists: ${selected.path}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ await startServer(selected.path, meta);
33
+ });
34
+
35
+ // canvas open [path] — scan current directory or open a direct path
36
+ program
37
+ .command('open [path]')
38
+ .description('Scan current directory for canvases, or open a path directly')
39
+ .option('-p, --port <port>', 'Server port', '4800')
40
+ .action(async (targetPath, opts) => {
41
+ portOverride = opts.port ? parseInt(opts.port) : null;
42
+
43
+ if (targetPath) {
44
+ const fullPath = resolve(targetPath);
45
+
46
+ if (!existsSync(fullPath)) {
47
+ console.error(` Directory not found: ${fullPath}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ // Direct canvas path
52
+ const meta = await tryReadCanvasMeta(fullPath);
53
+ if (meta) {
54
+ await startServer(meta.path, meta);
55
+ return;
56
+ }
57
+
58
+ // Not a canvas — scan it
59
+ console.log(' Scanning for canvases...');
60
+ const found = await scanForCanvases(fullPath, 4);
61
+ const history = await getRecentCanvases();
62
+ const selected = await fuzzySelectCanvas(found, history);
63
+ if (!selected) process.exit(0);
64
+ await startServer(selected.path, selected);
65
+ } else {
66
+ // No path — scan current directory
67
+ const cwd = process.cwd();
68
+ const cwdMeta = await tryReadCanvasMeta(cwd);
69
+
70
+ if (cwdMeta) {
71
+ await startServer(cwdMeta.path, cwdMeta);
72
+ return;
73
+ }
74
+
75
+ console.log(' Scanning for canvases...');
76
+ const found = await scanForCanvases(cwd, 4);
77
+ const history = await getRecentCanvases();
78
+ const selected = await fuzzySelectCanvas(found, history);
79
+ if (!selected) process.exit(0);
80
+ await startServer(selected.path, selected);
81
+ }
82
+ });
83
+
84
+ program
85
+ .command('list')
86
+ .description('Show running canvas instances')
87
+ .action(async () => {
88
+ const instances = await listRunningInstances();
89
+ if (instances.length === 0) {
90
+ console.log(' No running canvas instances.');
91
+ return;
92
+ }
93
+
94
+ console.log(`\n Running instances:\n`);
95
+ for (const inst of instances) {
96
+ console.log(` Port ${inst.port} PID ${inst.pid}`);
97
+ console.log(` ${inst.title || 'unknown'}`);
98
+ console.log(` ${inst.canvasDir}\n`);
99
+ }
100
+ });
101
+
102
+ program
103
+ .command('stop [port]')
104
+ .description('Stop canvas instance(s)')
105
+ .option('-a, --all', 'Stop all instances')
106
+ .action(async (portArg, opts) => {
107
+ if (opts.all) {
108
+ const instances = await listRunningInstances();
109
+ if (instances.length === 0) {
110
+ console.log(' No running instances.');
111
+ return;
112
+ }
113
+ for (const inst of instances) {
114
+ killPort(inst.port);
115
+ console.log(` Stopped port ${inst.port} (PID ${inst.pid})`);
116
+ }
117
+ return;
118
+ }
119
+
120
+ if (!portArg) {
121
+ const instances = await listRunningInstances();
122
+ if (instances.length === 0) {
123
+ console.log(' No running instances.');
124
+ return;
125
+ }
126
+ if (instances.length === 1) {
127
+ killPort(instances[0].port);
128
+ console.log(` Stopped port ${instances[0].port} (PID ${instances[0].pid})`);
129
+ return;
130
+ }
131
+ console.log(' Multiple instances running. Specify a port or use --all:');
132
+ for (const inst of instances) {
133
+ console.log(` canvas stop ${inst.port}`);
134
+ }
135
+ return;
136
+ }
137
+
138
+ const port = parseInt(portArg);
139
+ const occupant = getPortOccupant(port);
140
+ if (!occupant) {
141
+ console.log(` Nothing running on port ${port}`);
142
+ return;
143
+ }
144
+ killPort(port);
145
+ console.log(` Stopped port ${port} (PID ${occupant.pid})`);
146
+ });
147
+
148
+ let portOverride = null;
149
+
150
+ async function startServer(canvasDir, meta) {
151
+ const preferredPort = portOverride || 4800;
152
+ let port;
153
+
154
+ if (await isPortFree(preferredPort)) {
155
+ port = preferredPort;
156
+ } else {
157
+ const occupant = getPortOccupant(preferredPort);
158
+
159
+ if (occupant?.isPmCanvas) {
160
+ const action = await handlePortConflict(preferredPort, occupant);
161
+ if (!action) process.exit(0);
162
+
163
+ switch (action) {
164
+ case 'reuse':
165
+ const url = `http://localhost:${preferredPort}`;
166
+ console.log(`\n Opening ${url}`);
167
+ await open(url);
168
+ process.exit(0);
169
+ case 'kill':
170
+ killPort(preferredPort);
171
+ await new Promise(r => setTimeout(r, 500));
172
+ port = preferredPort;
173
+ break;
174
+ case 'next':
175
+ port = await findAvailablePort(preferredPort + 1);
176
+ break;
177
+ }
178
+ } else {
179
+ port = await findAvailablePort(preferredPort);
180
+ }
181
+ }
182
+
183
+ const server = createServer(canvasDir);
184
+ const title = meta?.title || canvasDir.split('/').pop();
185
+ server.listen(port, async () => {
186
+ const url = `http://localhost:${port}`;
187
+ console.log(`\n canvas-viewer`);
188
+ console.log(` Canvas: ${title}`);
189
+ console.log(` Path: ${canvasDir}`);
190
+ console.log(` URL: ${url}\n`);
191
+ open(url);
192
+
193
+ await registerInstance(port, canvasDir, title).catch(() => {});
194
+ await recordOpen(canvasDir, meta?.title).catch(() => {});
195
+ });
196
+ }
197
+
198
+ program.parse();