@leeoohoo/ui-apps-devkit 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/README.md +70 -0
- package/bin/chatos-uiapp.js +5 -0
- package/package.json +22 -0
- package/src/cli.js +53 -0
- package/src/commands/dev.js +14 -0
- package/src/commands/init.js +141 -0
- package/src/commands/install.js +55 -0
- package/src/commands/pack.js +72 -0
- package/src/commands/validate.js +103 -0
- package/src/lib/args.js +49 -0
- package/src/lib/config.js +29 -0
- package/src/lib/fs.js +78 -0
- package/src/lib/path-boundary.js +16 -0
- package/src/lib/plugin.js +45 -0
- package/src/lib/template.js +168 -0
- package/src/sandbox/server.js +861 -0
- package/templates/basic/README.md +58 -0
- package/templates/basic/chatos.config.json +5 -0
- package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
- package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
- package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
- package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
- package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
- package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
- package/templates/basic/plugin/apps/app/index.mjs +263 -0
- package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -0
- package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -0
- package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -0
- package/templates/basic/plugin/backend/index.mjs +37 -0
- package/templates/basic/template.json +7 -0
- package/templates/notepad/README.md +36 -0
- package/templates/notepad/chatos.config.json +4 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +181 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +74 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +123 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +110 -0
- package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +227 -0
- package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -0
- package/templates/notepad/plugin/apps/app/api.mjs +30 -0
- package/templates/notepad/plugin/apps/app/dom.mjs +14 -0
- package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -0
- package/templates/notepad/plugin/apps/app/index.mjs +1056 -0
- package/templates/notepad/plugin/apps/app/layers.mjs +338 -0
- package/templates/notepad/plugin/apps/app/markdown.mjs +120 -0
- package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -0
- package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -0
- package/templates/notepad/plugin/apps/app/mcp-server.mjs +200 -0
- package/templates/notepad/plugin/apps/app/styles.mjs +355 -0
- package/templates/notepad/plugin/apps/app/tags.mjs +21 -0
- package/templates/notepad/plugin/apps/app/ui.mjs +280 -0
- package/templates/notepad/plugin/backend/index.mjs +99 -0
- package/templates/notepad/plugin/plugin.json +23 -0
- package/templates/notepad/plugin/shared/notepad-paths.mjs +62 -0
- package/templates/notepad/plugin/shared/notepad-store.mjs +765 -0
- package/templates/notepad/template.json +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ChatOS UI Apps DevKit
|
|
2
|
+
|
|
3
|
+
一个可通过 npm 安装的 DevKit,用来:
|
|
4
|
+
|
|
5
|
+
- 生成 UI Apps 插件工程(脚手架)
|
|
6
|
+
- 在本地沙箱里运行/调试 `module` 应用(Host API mock)
|
|
7
|
+
- 校验 `plugin.json` 与路径边界
|
|
8
|
+
- 打包/安装到本机 ChatOS(`~/.deepseek_cli/chatos/ui_apps/plugins`)
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -g @leeoohoo/ui-apps-devkit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
或直接用 npx:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @leeoohoo/ui-apps-devkit chatos-uiapp --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
chatos-uiapp init my-first-uiapp
|
|
26
|
+
cd my-first-uiapp
|
|
27
|
+
npm install
|
|
28
|
+
npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 模板
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
chatos-uiapp init --list-templates
|
|
35
|
+
chatos-uiapp init my-app --template basic
|
|
36
|
+
chatos-uiapp init my-app --template notepad
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `basic`:最小可运行骨架(含 `host.chat.*` / `ctx.llm.complete()` 示例)
|
|
40
|
+
- `notepad`:完整示例应用(文件夹/标签/搜索/后端持久化)
|
|
41
|
+
|
|
42
|
+
完成开发后:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run validate
|
|
46
|
+
npm run pack
|
|
47
|
+
npm run install:chatos
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 生成项目结构(约定)
|
|
51
|
+
|
|
52
|
+
生成的工程里,**可安装产物**固定在 `plugin/` 目录:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
my-first-uiapp/
|
|
56
|
+
docs/ # 协议文档(随工程分发)
|
|
57
|
+
chatos.config.json # DevKit 配置(pluginDir/appId)
|
|
58
|
+
plugin/ # 直接导入/安装到 ChatOS 的插件目录
|
|
59
|
+
plugin.json
|
|
60
|
+
backend/ # (可选) Electron main 进程后端
|
|
61
|
+
apps/<appId>/ # module 前端 + AI 贡献(MCP/Prompt)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## CLI
|
|
65
|
+
|
|
66
|
+
- `chatos-uiapp init <dir>`:生成工程
|
|
67
|
+
- `chatos-uiapp dev`:启动本地运行沙箱(支持文件变更自动重载)
|
|
68
|
+
- `chatos-uiapp validate`:校验 manifest 与路径边界
|
|
69
|
+
- `chatos-uiapp pack`:打包 `.zip`(用于 ChatOS 导入)
|
|
70
|
+
- `chatos-uiapp install`:复制到本机 ChatOS 用户插件目录
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leeoohoo/ui-apps-devkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ChatOS UI Apps DevKit (CLI + templates + sandbox) for building installable ChatOS UI Apps plugins.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chatos-uiapp": "bin/chatos-uiapp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/**",
|
|
12
|
+
"src/**",
|
|
13
|
+
"templates/**",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cmdInit } from './commands/init.js';
|
|
2
|
+
import { cmdValidate } from './commands/validate.js';
|
|
3
|
+
import { cmdPack } from './commands/pack.js';
|
|
4
|
+
import { cmdInstall } from './commands/install.js';
|
|
5
|
+
import { cmdDev } from './commands/dev.js';
|
|
6
|
+
|
|
7
|
+
import { parseArgs } from './lib/args.js';
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
// Keep it short; detailed docs live in README and generated project.
|
|
11
|
+
// eslint-disable-next-line no-console
|
|
12
|
+
console.log(`chatos-uiapp
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
chatos-uiapp init <dir> [--template <name>] [--force] [--plugin-id <id>] [--name <name>] [--app-id <appId>] [--version <semver>]
|
|
16
|
+
chatos-uiapp init --list-templates
|
|
17
|
+
chatos-uiapp dev [--port 4399] [--app <appId>] [--plugin-dir <path>]
|
|
18
|
+
chatos-uiapp validate [--plugin-dir <path>]
|
|
19
|
+
chatos-uiapp pack [--out <zipPath>] [--plugin-dir <path>]
|
|
20
|
+
chatos-uiapp install [--host-app chatos] [--state-dir <path>] [--plugin-dir <path>]
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
chatos-uiapp init my-app
|
|
24
|
+
chatos-uiapp init my-app --template notepad
|
|
25
|
+
chatos-uiapp init --list-templates
|
|
26
|
+
chatos-uiapp dev --port 4399
|
|
27
|
+
chatos-uiapp install --host-app chatos
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runCli(argv) {
|
|
32
|
+
const { positionals, flags } = parseArgs(argv);
|
|
33
|
+
const cmd = String(positionals[0] || '').trim();
|
|
34
|
+
|
|
35
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || flags.help) {
|
|
36
|
+
printHelp();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (cmd === 'init') return await cmdInit({ positionals: positionals.slice(1), flags });
|
|
42
|
+
if (cmd === 'dev') return await cmdDev({ positionals: positionals.slice(1), flags });
|
|
43
|
+
if (cmd === 'validate') return await cmdValidate({ positionals: positionals.slice(1), flags });
|
|
44
|
+
if (cmd === 'pack') return await cmdPack({ positionals: positionals.slice(1), flags });
|
|
45
|
+
if (cmd === 'install') return await cmdInstall({ positionals: positionals.slice(1), flags });
|
|
46
|
+
|
|
47
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.error(`[chatos-uiapp] ${err?.message || String(err)}`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { loadDevkitConfig } from '../lib/config.js';
|
|
2
|
+
import { findPluginDir } from '../lib/plugin.js';
|
|
3
|
+
import { startSandboxServer } from '../sandbox/server.js';
|
|
4
|
+
|
|
5
|
+
export async function cmdDev({ flags }) {
|
|
6
|
+
const { config } = loadDevkitConfig(process.cwd());
|
|
7
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
8
|
+
const portRaw = String(flags.port || flags.p || '').trim();
|
|
9
|
+
const port = portRaw ? Number(portRaw) : 4399;
|
|
10
|
+
const appId = String(flags.app || flags['app-id'] || flags.appId || config?.appId || '').trim();
|
|
11
|
+
|
|
12
|
+
await startSandboxServer({ pluginDir, port, appId });
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
|
|
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';
|
|
14
|
+
|
|
15
|
+
function canPrompt() {
|
|
16
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function promptLine(question, { defaultValue = '' } = {}) {
|
|
20
|
+
if (!canPrompt()) throw new Error(`Missing required value: ${question}`);
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
try {
|
|
23
|
+
const answer = await new Promise((resolve) => rl.question(question, resolve));
|
|
24
|
+
const v = String(answer || '').trim();
|
|
25
|
+
return v || defaultValue;
|
|
26
|
+
} finally {
|
|
27
|
+
rl.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function cmdInit({ positionals, flags }) {
|
|
32
|
+
const list = Boolean(flags['list-templates'] || flags.listTemplates);
|
|
33
|
+
if (list) {
|
|
34
|
+
const templates = listTemplates();
|
|
35
|
+
if (templates.length === 0) {
|
|
36
|
+
// eslint-disable-next-line no-console
|
|
37
|
+
console.log('No templates found.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const lines = templates.map((t) => `- ${t.name}${t.description ? `: ${t.description}` : ''}`).join('\n');
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(`Templates:\n${lines}\n\nUse:\n chatos-uiapp init my-app --template <name>\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dirArg = String(positionals[0] || '').trim();
|
|
47
|
+
if (!dirArg) throw new Error('init requires <dir>');
|
|
48
|
+
|
|
49
|
+
const templateName = String(flags.template || flags.t || '').trim() || 'basic';
|
|
50
|
+
const templateMeta = readTemplateMeta(templateName);
|
|
51
|
+
|
|
52
|
+
const destDir = path.resolve(process.cwd(), dirArg);
|
|
53
|
+
const force = Boolean(flags.force);
|
|
54
|
+
|
|
55
|
+
if (isDirectory(destDir)) {
|
|
56
|
+
const entries = await (async () => {
|
|
57
|
+
try {
|
|
58
|
+
return (await import('fs')).default.readdirSync(destDir);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
if (entries.length > 0 && !force) {
|
|
64
|
+
throw new Error(`Target directory is not empty: ${destDir} (use --force to overwrite)`);
|
|
65
|
+
}
|
|
66
|
+
if (force) rmForce(destDir);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ensureDir(destDir);
|
|
70
|
+
copyTemplate({ templateName, destDir });
|
|
71
|
+
|
|
72
|
+
const pluginId =
|
|
73
|
+
String(flags['plugin-id'] || flags.pluginId || '').trim() || (await promptLine('pluginId (e.g. com.example.myapp): '));
|
|
74
|
+
const pluginName =
|
|
75
|
+
String(flags.name || '').trim() ||
|
|
76
|
+
(await promptLine('plugin name (display): ', { defaultValue: String(templateMeta?.defaults?.pluginName || '').trim() || pluginId }));
|
|
77
|
+
|
|
78
|
+
const defaultAppId = String(templateMeta?.defaults?.appId || '').trim() || 'app';
|
|
79
|
+
const appId =
|
|
80
|
+
String(flags['app-id'] || flags.appId || '').trim() ||
|
|
81
|
+
(await promptLine('appId (e.g. manager): ', { defaultValue: defaultAppId }));
|
|
82
|
+
|
|
83
|
+
const version = String(flags.version || '').trim() || String(templateMeta?.defaults?.version || '').trim() || '0.1.0';
|
|
84
|
+
|
|
85
|
+
const pluginDir = path.join(destDir, 'plugin');
|
|
86
|
+
ensureDir(pluginDir);
|
|
87
|
+
|
|
88
|
+
if (!isFile(path.join(pluginDir, 'plugin.json'))) {
|
|
89
|
+
const withBackend = templateMeta?.defaults?.withBackend !== false;
|
|
90
|
+
writeScaffoldManifest({ destPluginDir: pluginDir, pluginId, pluginName, version, appId, withBackend });
|
|
91
|
+
}
|
|
92
|
+
writeScaffoldPackageJson({ destDir, projectName: path.basename(destDir) });
|
|
93
|
+
writeScaffoldConfig({ destDir, pluginDir: 'plugin', appId });
|
|
94
|
+
|
|
95
|
+
// rename template app folder "app" -> actual appId
|
|
96
|
+
const srcAppDir = path.join(pluginDir, 'apps', 'app');
|
|
97
|
+
const dstAppDir = path.join(pluginDir, 'apps', appId);
|
|
98
|
+
try {
|
|
99
|
+
const fs = (await import('fs')).default;
|
|
100
|
+
if (fs.existsSync(srcAppDir) && fs.statSync(srcAppDir).isDirectory()) {
|
|
101
|
+
ensureDir(path.dirname(dstAppDir));
|
|
102
|
+
fs.renameSync(srcAppDir, dstAppDir);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Token replacements inside README/template code.
|
|
109
|
+
const replacements = {
|
|
110
|
+
__PLUGIN_ID__: pluginId,
|
|
111
|
+
__PLUGIN_NAME__: pluginName,
|
|
112
|
+
__APP_ID__: appId,
|
|
113
|
+
__VERSION__: version,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
maybeReplaceTokensInFile(path.join(destDir, 'README.md'), replacements);
|
|
117
|
+
maybeReplaceTokensInFile(path.join(destDir, 'chatos.config.json'), replacements);
|
|
118
|
+
if (isFile(path.join(pluginDir, 'plugin.json'))) {
|
|
119
|
+
maybeReplaceTokensInFile(path.join(pluginDir, 'plugin.json'), replacements);
|
|
120
|
+
}
|
|
121
|
+
maybeReplaceTokensInFile(path.join(pluginDir, 'backend', 'index.mjs'), replacements);
|
|
122
|
+
maybeReplaceTokensInFile(path.join(dstAppDir, 'index.mjs'), replacements);
|
|
123
|
+
maybeReplaceTokensInFile(path.join(dstAppDir, 'mcp-server.mjs'), replacements);
|
|
124
|
+
maybeReplaceTokensInFile(path.join(dstAppDir, 'mcp-prompt.zh.md'), replacements);
|
|
125
|
+
maybeReplaceTokensInFile(path.join(dstAppDir, 'mcp-prompt.en.md'), replacements);
|
|
126
|
+
|
|
127
|
+
// Ensure a helpful note exists even if template is edited later.
|
|
128
|
+
writeText(
|
|
129
|
+
path.join(destDir, '.gitignore'),
|
|
130
|
+
`node_modules/\n.DS_Store\n.chatos/\n*.log\n\n# build outputs (if you add bundling later)\ndist/\n`
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.log(`Created: ${destDir}
|
|
135
|
+
|
|
136
|
+
Next:
|
|
137
|
+
cd ${dirArg}
|
|
138
|
+
npm install
|
|
139
|
+
npm run dev
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
function defaultStateDir(hostApp) {
|
|
9
|
+
const app = typeof hostApp === 'string' && hostApp.trim() ? hostApp.trim() : 'chatos';
|
|
10
|
+
return path.join(os.homedir(), '.deepseek_cli', app);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function copyPluginDir(srcDir, destDir) {
|
|
14
|
+
ensureDir(path.dirname(destDir));
|
|
15
|
+
rmForce(destDir);
|
|
16
|
+
copyDir(srcDir, destDir, {
|
|
17
|
+
filter: (src) => {
|
|
18
|
+
const base = path.basename(src);
|
|
19
|
+
if (base === 'node_modules') return false;
|
|
20
|
+
if (base === '.git') return false;
|
|
21
|
+
if (base === '.DS_Store') return false;
|
|
22
|
+
if (base.endsWith('.map')) return false;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function cmdInstall({ flags }) {
|
|
29
|
+
const { config } = loadDevkitConfig(process.cwd());
|
|
30
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
31
|
+
const { pluginId, name, version } = loadPluginManifest(pluginDir);
|
|
32
|
+
|
|
33
|
+
const hostApp = String(flags['host-app'] || flags.hostApp || 'chatos').trim() || 'chatos';
|
|
34
|
+
const stateDir = String(flags['state-dir'] || flags.stateDir || defaultStateDir(hostApp)).trim();
|
|
35
|
+
if (!stateDir) throw new Error('stateDir is required');
|
|
36
|
+
|
|
37
|
+
const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
|
|
38
|
+
ensureDir(pluginsRoot);
|
|
39
|
+
|
|
40
|
+
const dirName = sanitizeDirComponent(pluginId);
|
|
41
|
+
if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
|
|
42
|
+
|
|
43
|
+
const destDir = path.join(pluginsRoot, dirName);
|
|
44
|
+
const replaced = isDirectory(destDir);
|
|
45
|
+
copyPluginDir(pluginDir, destDir);
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(
|
|
49
|
+
`Installed: ${pluginId} (${name}@${version})\n` +
|
|
50
|
+
` -> ${destDir}\n` +
|
|
51
|
+
` replaced: ${replaced}\n\n` +
|
|
52
|
+
`Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
import { copyDir, ensureDir, rmForce } from '../lib/fs.js';
|
|
6
|
+
import { loadDevkitConfig } from '../lib/config.js';
|
|
7
|
+
import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
|
|
8
|
+
|
|
9
|
+
function hasCmd(cmd) {
|
|
10
|
+
const res = spawnSync('sh', ['-lc', `command -v ${cmd} >/dev/null 2>&1`], { stdio: 'ignore' });
|
|
11
|
+
return res.status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function packWithZip({ cwd, srcDir, outFile }) {
|
|
15
|
+
// zip -r out.zip . (from within srcDir)
|
|
16
|
+
const res = spawnSync('zip', ['-r', outFile, '.'], { cwd: srcDir, stdio: 'inherit' });
|
|
17
|
+
if (res.status !== 0) throw new Error('zip failed');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function packWithPowershell({ srcDir, outFile }) {
|
|
21
|
+
const script = `Compress-Archive -Path "${srcDir}\\*" -DestinationPath "${outFile}" -Force`;
|
|
22
|
+
const res = spawnSync('powershell', ['-NoProfile', '-Command', script], { stdio: 'inherit' });
|
|
23
|
+
if (res.status !== 0) throw new Error('powershell Compress-Archive failed');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function cmdPack({ flags }) {
|
|
27
|
+
const { config } = loadDevkitConfig(process.cwd());
|
|
28
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
29
|
+
const { pluginId, version } = loadPluginManifest(pluginDir);
|
|
30
|
+
|
|
31
|
+
const outArg = String(flags.out || '').trim();
|
|
32
|
+
const outDir = outArg ? path.dirname(path.resolve(process.cwd(), outArg)) : path.join(process.cwd(), 'dist');
|
|
33
|
+
ensureDir(outDir);
|
|
34
|
+
|
|
35
|
+
const outFile = outArg
|
|
36
|
+
? path.resolve(process.cwd(), outArg)
|
|
37
|
+
: path.join(outDir, `${pluginId.replace(/[^a-zA-Z0-9._-]+/g, '-')}-${version || '0.0.0'}.zip`);
|
|
38
|
+
|
|
39
|
+
rmForce(outFile);
|
|
40
|
+
|
|
41
|
+
const stagingBase = path.join(os.tmpdir(), `chatos-uiapp-pack-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
42
|
+
ensureDir(stagingBase);
|
|
43
|
+
try {
|
|
44
|
+
// mimic ChatOS importer: exclude node_modules/.git/*.map/.DS_Store
|
|
45
|
+
copyDir(pluginDir, stagingBase, {
|
|
46
|
+
filter: (src) => {
|
|
47
|
+
const base = path.basename(src);
|
|
48
|
+
if (base === 'node_modules') return false;
|
|
49
|
+
if (base === '.git') return false;
|
|
50
|
+
if (base === '.DS_Store') return false;
|
|
51
|
+
if (base.endsWith('.map')) return false;
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const platform = process.platform;
|
|
57
|
+
if (hasCmd('zip')) {
|
|
58
|
+
packWithZip({ cwd: process.cwd(), srcDir: stagingBase, outFile });
|
|
59
|
+
} else if (platform === 'win32') {
|
|
60
|
+
packWithPowershell({ srcDir: stagingBase, outFile });
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error('zip command not found (install "zip" or run on Windows with powershell)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log(`Packed: ${outFile}
|
|
67
|
+
|
|
68
|
+
ChatOS -> 应用 -> 导入应用包 -> 选择该 zip`);
|
|
69
|
+
} finally {
|
|
70
|
+
rmForce(stagingBase);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { isFile } from '../lib/fs.js';
|
|
5
|
+
import { loadDevkitConfig } from '../lib/config.js';
|
|
6
|
+
import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
|
|
7
|
+
import { resolveInsideDir } from '../lib/path-boundary.js';
|
|
8
|
+
|
|
9
|
+
function assert(cond, message) {
|
|
10
|
+
if (!cond) throw new Error(message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function statSizeSafe(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(filePath).size;
|
|
16
|
+
} catch {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function cmdValidate({ flags }) {
|
|
22
|
+
const { config } = loadDevkitConfig(process.cwd());
|
|
23
|
+
const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
|
|
24
|
+
|
|
25
|
+
const { manifestPath, manifest } = loadPluginManifest(pluginDir);
|
|
26
|
+
|
|
27
|
+
const manifestSize = statSizeSafe(manifestPath);
|
|
28
|
+
assert(manifestSize <= 256 * 1024, `plugin.json too large (>256KiB): ${manifestSize} bytes`);
|
|
29
|
+
|
|
30
|
+
assert(Number(manifest?.manifestVersion || 1) === 1, 'manifestVersion must be 1');
|
|
31
|
+
assert(typeof manifest?.id === 'string' && manifest.id.trim(), 'plugin.id is required');
|
|
32
|
+
assert(typeof manifest?.name === 'string' && manifest.name.trim(), 'plugin.name is required');
|
|
33
|
+
|
|
34
|
+
if (manifest?.backend?.entry) {
|
|
35
|
+
const backendAbs = resolveInsideDir(pluginDir, manifest.backend.entry);
|
|
36
|
+
assert(isFile(backendAbs), `backend.entry must be a file: ${manifest.backend.entry}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const apps = Array.isArray(manifest?.apps) ? manifest.apps : [];
|
|
40
|
+
assert(apps.length > 0, 'plugin.apps[] is required (>=1)');
|
|
41
|
+
const ids = new Set();
|
|
42
|
+
for (const app of apps) {
|
|
43
|
+
const appId = typeof app?.id === 'string' ? app.id.trim() : '';
|
|
44
|
+
assert(appId, 'apps[i].id is required');
|
|
45
|
+
assert(!ids.has(appId), `duplicate app.id: ${appId}`);
|
|
46
|
+
ids.add(appId);
|
|
47
|
+
|
|
48
|
+
assert(typeof app?.name === 'string' && app.name.trim(), `apps[${appId}].name is required`);
|
|
49
|
+
assert(app?.entry?.type === 'module', `apps[${appId}].entry.type must be "module"`);
|
|
50
|
+
const entryPath = typeof app?.entry?.path === 'string' ? app.entry.path.trim() : '';
|
|
51
|
+
assert(entryPath, `apps[${appId}].entry.path is required`);
|
|
52
|
+
const entryAbs = resolveInsideDir(pluginDir, entryPath);
|
|
53
|
+
assert(isFile(entryAbs), `apps[${appId}].entry.path must be a file: ${entryPath}`);
|
|
54
|
+
|
|
55
|
+
// Basic ai path boundary checks (full schema is defined in docs; this validates the security boundary).
|
|
56
|
+
const ai = app?.ai;
|
|
57
|
+
const aiObj =
|
|
58
|
+
typeof ai === 'string'
|
|
59
|
+
? { config: ai }
|
|
60
|
+
: ai && typeof ai === 'object'
|
|
61
|
+
? ai
|
|
62
|
+
: null;
|
|
63
|
+
|
|
64
|
+
if (aiObj?.config) {
|
|
65
|
+
const abs = resolveInsideDir(pluginDir, aiObj.config);
|
|
66
|
+
assert(isFile(abs), `apps[${appId}].ai.config must be a file: ${aiObj.config}`);
|
|
67
|
+
const size = statSizeSafe(abs);
|
|
68
|
+
assert(size <= 128 * 1024, `ai.config too large (>128KiB): ${aiObj.config}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (aiObj?.mcp?.entry) {
|
|
72
|
+
const abs = resolveInsideDir(pluginDir, aiObj.mcp.entry);
|
|
73
|
+
assert(isFile(abs), `apps[${appId}].ai.mcp.entry must be a file: ${aiObj.mcp.entry}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const mcpPrompt = aiObj?.mcpPrompt;
|
|
77
|
+
const collectPromptPaths = () => {
|
|
78
|
+
if (!mcpPrompt) return [];
|
|
79
|
+
if (typeof mcpPrompt === 'string') return [mcpPrompt];
|
|
80
|
+
if (typeof mcpPrompt !== 'object') return [];
|
|
81
|
+
const zh = mcpPrompt.zh;
|
|
82
|
+
const en = mcpPrompt.en;
|
|
83
|
+
const out = [];
|
|
84
|
+
const pushSource = (src) => {
|
|
85
|
+
if (!src) return;
|
|
86
|
+
if (typeof src === 'string') out.push(src);
|
|
87
|
+
else if (typeof src === 'object' && typeof src.path === 'string') out.push(src.path);
|
|
88
|
+
};
|
|
89
|
+
pushSource(zh);
|
|
90
|
+
pushSource(en);
|
|
91
|
+
return out;
|
|
92
|
+
};
|
|
93
|
+
for (const rel of collectPromptPaths()) {
|
|
94
|
+
const abs = resolveInsideDir(pluginDir, rel);
|
|
95
|
+
assert(isFile(abs), `apps[${appId}].ai.mcpPrompt path must be a file: ${rel}`);
|
|
96
|
+
const size = statSizeSafe(abs);
|
|
97
|
+
assert(size <= 128 * 1024, `mcpPrompt too large (>128KiB): ${rel}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
|
|
103
|
+
}
|
package/src/lib/args.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const raw = Array.isArray(argv) ? argv.slice(2) : [];
|
|
3
|
+
const flags = {};
|
|
4
|
+
const positionals = [];
|
|
5
|
+
|
|
6
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
7
|
+
const token = raw[i];
|
|
8
|
+
if (!token) continue;
|
|
9
|
+
|
|
10
|
+
if (token === '--') {
|
|
11
|
+
positionals.push(...raw.slice(i + 1));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (token.startsWith('--')) {
|
|
16
|
+
const eq = token.indexOf('=');
|
|
17
|
+
const key = (eq >= 0 ? token.slice(2, eq) : token.slice(2)).trim();
|
|
18
|
+
const value = eq >= 0 ? token.slice(eq + 1) : raw[i + 1];
|
|
19
|
+
if (!key) continue;
|
|
20
|
+
|
|
21
|
+
if (eq < 0 && value && !value.startsWith('-')) {
|
|
22
|
+
flags[key] = value;
|
|
23
|
+
i += 1;
|
|
24
|
+
} else if (eq >= 0) {
|
|
25
|
+
flags[key] = value;
|
|
26
|
+
} else {
|
|
27
|
+
flags[key] = true;
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
33
|
+
const key = token.slice(1).trim();
|
|
34
|
+
const value = raw[i + 1];
|
|
35
|
+
if (value && !value.startsWith('-')) {
|
|
36
|
+
flags[key] = value;
|
|
37
|
+
i += 1;
|
|
38
|
+
} else {
|
|
39
|
+
flags[key] = true;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
positionals.push(token);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { positionals, flags };
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { isFile, readJson } from './fs.js';
|
|
5
|
+
|
|
6
|
+
export function loadDevkitConfig(cwd) {
|
|
7
|
+
const root = typeof cwd === 'string' ? cwd : process.cwd();
|
|
8
|
+
const cfgPath = path.join(root, 'chatos.config.json');
|
|
9
|
+
if (!isFile(cfgPath)) return { path: cfgPath, config: null };
|
|
10
|
+
try {
|
|
11
|
+
const cfg = readJson(cfgPath);
|
|
12
|
+
return { path: cfgPath, config: cfg };
|
|
13
|
+
} catch {
|
|
14
|
+
return { path: cfgPath, config: null };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readOptionalJson(filePath) {
|
|
19
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
20
|
+
if (!normalized) return null;
|
|
21
|
+
if (!fs.existsSync(normalized)) return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = fs.readFileSync(normalized, 'utf8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|