@leeoohoo/ui-apps-devkit 0.1.1 → 0.1.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 +25 -16
- package/package.json +6 -3
- package/src/commands/init.js +14 -13
- package/src/commands/install.js +12 -11
- package/src/commands/validate.js +69 -21
- package/src/lib/state-constants.js +2 -0
- package/src/sandbox/server.js +1145 -219
- package/templates/basic/README.md +19 -14
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +36 -34
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +3 -2
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +1 -1
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +20 -18
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +23 -22
- package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +5 -5
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +3 -3
- package/templates/notepad/README.md +28 -19
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +36 -34
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +3 -2
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +1 -1
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +20 -18
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +23 -22
- package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +5 -5
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +3 -3
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +8 -1
- package/templates/notepad/plugin/shared/notepad-paths.mjs +41 -23
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- 生成 UI Apps 插件工程(脚手架)
|
|
6
6
|
- 在本地沙箱里运行/调试 `module` 应用(Host API mock)
|
|
7
7
|
- 校验 `plugin.json` 与路径边界
|
|
8
|
-
- 打包/安装到本机 ChatOS
|
|
8
|
+
- 打包/安装到本机 ChatOS(用户插件目录:`<stateDir>/ui_apps/plugins`;`stateDir = <stateRoot>/<hostApp>`)
|
|
9
9
|
|
|
10
10
|
## 安装
|
|
11
11
|
|
|
@@ -28,11 +28,13 @@ npm install
|
|
|
28
28
|
npm run dev
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## 沙箱能力
|
|
32
|
-
|
|
33
|
-
- 模拟 `module mount()` 与 `host.*` API
|
|
34
|
-
- 右上角 Theme 切换(light/dark/system),用于测试 `host.theme.onChange` 与样式响应
|
|
35
|
-
- Inspect 面板展示 `host.context` 与 `--ds-*` tokens
|
|
31
|
+
## 沙箱能力
|
|
32
|
+
|
|
33
|
+
- 模拟 `module mount()` 与 `host.*` API
|
|
34
|
+
- 右上角 Theme 切换(light/dark/system),用于测试 `host.theme.onChange` 与样式响应
|
|
35
|
+
- Inspect 面板展示 `host.context` 与 `--ds-*` tokens
|
|
36
|
+
- 右上角 `AI Config` 可配置 `API Key / Base URL / Model ID`,用于在沙箱内调用真实模型并测试应用 MCP(需配置 `ai.mcp`)
|
|
37
|
+
- `AI Config` 可设置 `Workdir` 覆盖(默认 `dataDir`,支持 `$dataDir/$pluginDir/$projectRoot`)
|
|
36
38
|
|
|
37
39
|
## 模板
|
|
38
40
|
|
|
@@ -45,15 +47,22 @@ chatos-uiapp init my-app --template notepad
|
|
|
45
47
|
- `basic`:最小可运行骨架(含 `host.chat.*` / `ctx.llm.complete()` 示例)
|
|
46
48
|
- `notepad`:完整示例应用(文件夹/标签/搜索/后端持久化)
|
|
47
49
|
|
|
48
|
-
完成开发后:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
npm run validate
|
|
52
|
-
npm run pack
|
|
53
|
-
npm run install:chatos
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
##
|
|
50
|
+
完成开发后:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run validate
|
|
54
|
+
npm run pack
|
|
55
|
+
npm run install:chatos
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## MCP 依赖(必读)
|
|
59
|
+
|
|
60
|
+
- ChatOS 导入插件时会排除 `node_modules/`,MCP server 运行时无法读取随包依赖。
|
|
61
|
+
- MCP server 只要引入第三方依赖(如 `@modelcontextprotocol/sdk`、`zod`),就必须在 build 阶段 **bundle 成单文件**,并在 `plugin.json` 的 `ai.mcp.entry` 指向 bundle 产物。
|
|
62
|
+
- 如果 MCP 启动报错 `Cannot find package '@modelcontextprotocol/sdk'`,说明依赖未被 bundle 或 vendoring 到插件目录。
|
|
63
|
+
- 或者完全使用 Node 内置模块,或把依赖源码 vendoring 到插件目录内。
|
|
64
|
+
|
|
65
|
+
## 生成项目结构(约定)
|
|
57
66
|
|
|
58
67
|
生成的工程里,**可安装产物**固定在 `plugin/` 目录:
|
|
59
68
|
|
|
@@ -71,6 +80,6 @@ my-first-uiapp/
|
|
|
71
80
|
|
|
72
81
|
- `chatos-uiapp init <dir>`:生成工程
|
|
73
82
|
- `chatos-uiapp dev`:启动本地运行沙箱(支持文件变更自动重载)
|
|
74
|
-
- `chatos-uiapp validate`:校验 manifest
|
|
83
|
+
- `chatos-uiapp validate`:校验 manifest 与路径边界(遇到 symlink 会给出警告)
|
|
75
84
|
- `chatos-uiapp pack`:打包 `.zip`(用于 ChatOS 导入)
|
|
76
85
|
- `chatos-uiapp install`:复制到本机 ChatOS 用户插件目录
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leeoohoo/ui-apps-devkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "ChatOS UI Apps DevKit (CLI + templates + sandbox) for building installable ChatOS UI Apps plugins.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"
|
|
6
|
+
"type": "module",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@modelcontextprotocol/sdk": "^1.25.2"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
8
11
|
"test": "node --test"
|
|
9
12
|
},
|
|
10
13
|
"bin": {
|
package/src/commands/init.js
CHANGED
|
@@ -2,15 +2,16 @@ import path from 'path';
|
|
|
2
2
|
import readline from 'readline';
|
|
3
3
|
|
|
4
4
|
import { ensureDir, isDirectory, isFile, rmForce, writeText } from '../lib/fs.js';
|
|
5
|
-
import {
|
|
6
|
-
copyTemplate,
|
|
7
|
-
listTemplates,
|
|
8
|
-
readTemplateMeta,
|
|
9
|
-
maybeReplaceTokensInFile,
|
|
10
|
-
writeScaffoldConfig,
|
|
11
|
-
writeScaffoldManifest,
|
|
12
|
-
writeScaffoldPackageJson,
|
|
13
|
-
} from '../lib/template.js';
|
|
5
|
+
import {
|
|
6
|
+
copyTemplate,
|
|
7
|
+
listTemplates,
|
|
8
|
+
readTemplateMeta,
|
|
9
|
+
maybeReplaceTokensInFile,
|
|
10
|
+
writeScaffoldConfig,
|
|
11
|
+
writeScaffoldManifest,
|
|
12
|
+
writeScaffoldPackageJson,
|
|
13
|
+
} from '../lib/template.js';
|
|
14
|
+
import { COMPAT_STATE_ROOT_DIRNAME, STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
14
15
|
|
|
15
16
|
function canPrompt() {
|
|
16
17
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -126,10 +127,10 @@ export async function cmdInit({ positionals, flags }) {
|
|
|
126
127
|
maybeReplaceTokensInFile(path.join(dstAppDir, 'mcp-prompt.en.md'), replacements);
|
|
127
128
|
|
|
128
129
|
// Ensure a helpful note exists even if template is edited later.
|
|
129
|
-
writeText(
|
|
130
|
-
path.join(destDir, '.gitignore'),
|
|
131
|
-
`node_modules/\n.DS_Store\n
|
|
132
|
-
);
|
|
130
|
+
writeText(
|
|
131
|
+
path.join(destDir, '.gitignore'),
|
|
132
|
+
`node_modules/\n.DS_Store\n${STATE_ROOT_DIRNAME}/\n${COMPAT_STATE_ROOT_DIRNAME}/\n*.log\n\n# build outputs (if you add bundling later)\ndist/\n`
|
|
133
|
+
);
|
|
133
134
|
|
|
134
135
|
// eslint-disable-next-line no-console
|
|
135
136
|
console.log(`Created: ${destDir}
|
package/src/commands/install.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import { copyDir, ensureDir, isDirectory, rmForce, sanitizeDirComponent } from '../lib/fs.js';
|
|
5
|
-
import { loadDevkitConfig } from '../lib/config.js';
|
|
6
|
-
import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { copyDir, ensureDir, isDirectory, rmForce, sanitizeDirComponent } from '../lib/fs.js';
|
|
5
|
+
import { loadDevkitConfig } from '../lib/config.js';
|
|
6
|
+
import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
|
|
7
|
+
import { STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
|
|
8
|
+
|
|
9
|
+
function defaultStateDir(hostApp) {
|
|
10
|
+
const app = typeof hostApp === 'string' && hostApp.trim() ? hostApp.trim() : 'chatos';
|
|
11
|
+
return path.join(os.homedir(), STATE_ROOT_DIRNAME, app);
|
|
12
|
+
}
|
|
12
13
|
|
|
13
14
|
function copyPluginDir(srcDir, destDir) {
|
|
14
15
|
ensureDir(path.dirname(destDir));
|
package/src/commands/validate.js
CHANGED
|
@@ -10,22 +10,63 @@ function assert(cond, message) {
|
|
|
10
10
|
if (!cond) throw new Error(message);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function statSizeSafe(filePath) {
|
|
14
|
-
try {
|
|
15
|
-
return fs.statSync(filePath).size;
|
|
16
|
-
} catch {
|
|
17
|
-
return 0;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
function statSizeSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(filePath).size;
|
|
16
|
+
} catch {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectSymlinks(rootDir) {
|
|
22
|
+
const root = typeof rootDir === 'string' ? rootDir.trim() : '';
|
|
23
|
+
if (!root) return [];
|
|
24
|
+
const out = [];
|
|
25
|
+
const walk = (dir) => {
|
|
26
|
+
let entries = [];
|
|
27
|
+
try {
|
|
28
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
entries.forEach((entry) => {
|
|
33
|
+
if (!entry || !entry.name) return;
|
|
34
|
+
const fullPath = path.join(dir, entry.name);
|
|
35
|
+
let stat;
|
|
36
|
+
try {
|
|
37
|
+
stat = fs.lstatSync(fullPath);
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (stat.isSymbolicLink()) {
|
|
42
|
+
out.push(fullPath);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
walk(fullPath);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
walk(root);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function cmdValidate({ flags }) {
|
|
55
|
+
const { config } = loadDevkitConfig(process.cwd());
|
|
56
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
57
|
+
|
|
58
|
+
const { manifestPath, manifest } = loadPluginManifest(pluginDir);
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const warn = (message) => warnings.push(message);
|
|
61
|
+
|
|
62
|
+
const symlinks = collectSymlinks(pluginDir);
|
|
63
|
+
if (symlinks.length > 0) {
|
|
64
|
+
const rel = symlinks.map((entry) => path.relative(pluginDir, entry) || entry);
|
|
65
|
+
warn(`Symlink detected inside plugin dir (may bypass path boundaries): ${rel.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const manifestSize = statSizeSafe(manifestPath);
|
|
69
|
+
assert(manifestSize <= 256 * 1024, `plugin.json too large (>256KiB): ${manifestSize} bytes`);
|
|
29
70
|
|
|
30
71
|
assert(Number(manifest?.manifestVersion || 1) === 1, 'manifestVersion must be 1');
|
|
31
72
|
assert(typeof manifest?.id === 'string' && manifest.id.trim(), 'plugin.id is required');
|
|
@@ -106,8 +147,15 @@ export async function cmdValidate({ flags }) {
|
|
|
106
147
|
const size = statSizeSafe(abs);
|
|
107
148
|
assert(size <= 128 * 1024, `mcpPrompt too large (>128KiB): ${rel}`);
|
|
108
149
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (warnings.length > 0) {
|
|
153
|
+
warnings.forEach((message) => {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.warn(`WARN: ${message}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
|
|
161
|
+
}
|