@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/src/lib/fs.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function isFile(filePath) {
|
|
5
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
6
|
+
if (!normalized) return false;
|
|
7
|
+
try {
|
|
8
|
+
return fs.existsSync(normalized) && fs.statSync(normalized).isFile();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isDirectory(dirPath) {
|
|
15
|
+
const normalized = typeof dirPath === 'string' ? dirPath.trim() : '';
|
|
16
|
+
if (!normalized) return false;
|
|
17
|
+
try {
|
|
18
|
+
return fs.existsSync(normalized) && fs.statSync(normalized).isDirectory();
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureDir(dirPath) {
|
|
25
|
+
const normalized = typeof dirPath === 'string' ? dirPath.trim() : '';
|
|
26
|
+
if (!normalized) return;
|
|
27
|
+
fs.mkdirSync(normalized, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readText(filePath) {
|
|
31
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
32
|
+
if (!normalized) throw new Error('readText: filePath is required');
|
|
33
|
+
return fs.readFileSync(normalized, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeText(filePath, content) {
|
|
37
|
+
const normalized = typeof filePath === 'string' ? filePath.trim() : '';
|
|
38
|
+
if (!normalized) throw new Error('writeText: filePath is required');
|
|
39
|
+
ensureDir(path.dirname(normalized));
|
|
40
|
+
fs.writeFileSync(normalized, content, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readJson(filePath) {
|
|
44
|
+
const raw = readText(filePath);
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (!parsed || typeof parsed !== 'object') throw new Error(`Invalid JSON object: ${filePath}`);
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeJson(filePath, obj) {
|
|
51
|
+
writeText(filePath, `${JSON.stringify(obj, null, 2)}\n`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function rmForce(targetPath) {
|
|
55
|
+
const normalized = typeof targetPath === 'string' ? targetPath.trim() : '';
|
|
56
|
+
if (!normalized) return;
|
|
57
|
+
try {
|
|
58
|
+
fs.rmSync(normalized, { recursive: true, force: true });
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function copyDir(srcDir, destDir, { filter } = {}) {
|
|
65
|
+
fs.cpSync(srcDir, destDir, {
|
|
66
|
+
recursive: true,
|
|
67
|
+
force: true,
|
|
68
|
+
dereference: false,
|
|
69
|
+
filter: (src) => (typeof filter === 'function' ? filter(src) : true),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function sanitizeDirComponent(value) {
|
|
74
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
75
|
+
if (!raw) return '';
|
|
76
|
+
return raw.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
export function resolveInsideDir(rootDir, relativePath) {
|
|
4
|
+
const root = typeof rootDir === 'string' ? rootDir.trim() : '';
|
|
5
|
+
const rel = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
6
|
+
if (!root) throw new Error('rootDir is required');
|
|
7
|
+
if (!rel) throw new Error('relativePath is required');
|
|
8
|
+
const resolved = path.resolve(root, rel);
|
|
9
|
+
const normalizedRoot = path.resolve(root);
|
|
10
|
+
const isInside = resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
|
|
11
|
+
if (!isInside) {
|
|
12
|
+
throw new Error(`Path escapes plugin dir: ${relativePath}`);
|
|
13
|
+
}
|
|
14
|
+
return resolved;
|
|
15
|
+
}
|
|
16
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import { isFile, readJson } from './fs.js';
|
|
4
|
+
|
|
5
|
+
export function findPluginDir(cwd, explicitPluginDir) {
|
|
6
|
+
const root = typeof cwd === 'string' ? cwd : process.cwd();
|
|
7
|
+
if (explicitPluginDir) {
|
|
8
|
+
const abs = path.resolve(root, explicitPluginDir);
|
|
9
|
+
if (!isFile(path.join(abs, 'plugin.json'))) {
|
|
10
|
+
throw new Error(`plugin.json not found in --plugin-dir: ${abs}`);
|
|
11
|
+
}
|
|
12
|
+
return abs;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const direct = path.join(root, 'plugin.json');
|
|
16
|
+
if (isFile(direct)) return root;
|
|
17
|
+
|
|
18
|
+
const nested = path.join(root, 'plugin', 'plugin.json');
|
|
19
|
+
if (isFile(nested)) return path.join(root, 'plugin');
|
|
20
|
+
|
|
21
|
+
throw new Error('Cannot find plugin.json (expected ./plugin.json or ./plugin/plugin.json)');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadPluginManifest(pluginDir) {
|
|
25
|
+
const manifestPath = path.join(pluginDir, 'plugin.json');
|
|
26
|
+
const manifest = readJson(manifestPath);
|
|
27
|
+
const pluginId = typeof manifest?.id === 'string' ? manifest.id.trim() : '';
|
|
28
|
+
const name = typeof manifest?.name === 'string' ? manifest.name.trim() : '';
|
|
29
|
+
const version = typeof manifest?.version === 'string' ? manifest.version.trim() : '0.0.0';
|
|
30
|
+
if (!pluginId) throw new Error(`plugin.json missing "id": ${manifestPath}`);
|
|
31
|
+
if (!name) throw new Error(`plugin.json missing "name": ${manifestPath}`);
|
|
32
|
+
return { manifestPath, manifest, pluginId, name, version };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pickAppFromManifest(manifest, preferredAppId) {
|
|
36
|
+
const apps = Array.isArray(manifest?.apps) ? manifest.apps : [];
|
|
37
|
+
if (apps.length === 0) throw new Error('plugin.json apps[] is empty');
|
|
38
|
+
if (preferredAppId) {
|
|
39
|
+
const hit = apps.find((a) => String(a?.id || '') === String(preferredAppId));
|
|
40
|
+
if (!hit) throw new Error(`app not found in plugin.json: ${preferredAppId}`);
|
|
41
|
+
return hit;
|
|
42
|
+
}
|
|
43
|
+
return apps[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
import { copyDir, ensureDir, isDirectory, isFile, readJson, writeJson, writeText } from './fs.js';
|
|
6
|
+
|
|
7
|
+
function packageRoot() {
|
|
8
|
+
// src/lib/template.js -> src -> package root
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
return path.resolve(here, '..', '..');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getTemplateDir(name) {
|
|
14
|
+
const root = packageRoot();
|
|
15
|
+
const dir = path.join(root, 'templates', name);
|
|
16
|
+
if (!isDirectory(dir)) throw new Error(`template not found: ${name}`);
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readSelfPackage() {
|
|
21
|
+
const root = packageRoot();
|
|
22
|
+
const pkgPath = path.join(root, 'package.json');
|
|
23
|
+
if (!isFile(pkgPath)) return { name: '@leeoohoo/ui-apps-devkit', version: '0.1.0' };
|
|
24
|
+
try {
|
|
25
|
+
const pkg = readJson(pkgPath);
|
|
26
|
+
return pkg && typeof pkg === 'object' ? pkg : { name: '@leeoohoo/ui-apps-devkit', version: '0.1.0' };
|
|
27
|
+
} catch {
|
|
28
|
+
return { name: '@leeoohoo/ui-apps-devkit', version: '0.1.0' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readTemplateMeta(name) {
|
|
33
|
+
const dir = getTemplateDir(name);
|
|
34
|
+
const metaPath = path.join(dir, 'template.json');
|
|
35
|
+
if (!isFile(metaPath)) return { name, description: '', defaults: null };
|
|
36
|
+
try {
|
|
37
|
+
const meta = readJson(metaPath);
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
description: typeof meta?.description === 'string' ? meta.description.trim() : '',
|
|
41
|
+
defaults: meta?.defaults && typeof meta.defaults === 'object' ? meta.defaults : null,
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return { name, description: '', defaults: null };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function listTemplates() {
|
|
49
|
+
const root = packageRoot();
|
|
50
|
+
const templatesDir = path.join(root, 'templates');
|
|
51
|
+
let entries = [];
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(templatesDir, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const ent of entries) {
|
|
60
|
+
if (!ent?.isDirectory?.()) continue;
|
|
61
|
+
const name = String(ent.name || '').trim();
|
|
62
|
+
if (!name || name.startsWith('.')) continue;
|
|
63
|
+
if (!isDirectory(path.join(templatesDir, name))) continue;
|
|
64
|
+
out.push(readTemplateMeta(name));
|
|
65
|
+
}
|
|
66
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function copyTemplate({ templateName, destDir }) {
|
|
71
|
+
const srcDir = getTemplateDir(templateName);
|
|
72
|
+
ensureDir(destDir);
|
|
73
|
+
|
|
74
|
+
copyDir(srcDir, destDir, {
|
|
75
|
+
filter: (src) => {
|
|
76
|
+
const base = path.basename(src);
|
|
77
|
+
if (base === 'node_modules') return false;
|
|
78
|
+
if (base === '.DS_Store') return false;
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function writeScaffoldManifest({ destPluginDir, pluginId, pluginName, version, appId, withBackend = true }) {
|
|
85
|
+
const manifest = {
|
|
86
|
+
manifestVersion: 1,
|
|
87
|
+
id: pluginId,
|
|
88
|
+
name: pluginName,
|
|
89
|
+
version: version || '0.1.0',
|
|
90
|
+
description: 'A ChatOS UI Apps plugin.',
|
|
91
|
+
...(withBackend ? { backend: { entry: 'backend/index.mjs' } } : {}),
|
|
92
|
+
apps: [
|
|
93
|
+
{
|
|
94
|
+
id: appId,
|
|
95
|
+
name: 'My App',
|
|
96
|
+
description: 'A ChatOS module app.',
|
|
97
|
+
entry: { type: 'module', path: `apps/${appId}/index.mjs` },
|
|
98
|
+
ai: {
|
|
99
|
+
// Keep the default scaffold dependency-free: prompt is safe, MCP server is opt-in.
|
|
100
|
+
mcpPrompt: {
|
|
101
|
+
title: 'My App · MCP Prompt',
|
|
102
|
+
zh: `apps/${appId}/mcp-prompt.zh.md`,
|
|
103
|
+
en: `apps/${appId}/mcp-prompt.en.md`,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
writeJson(path.join(destPluginDir, 'plugin.json'), manifest);
|
|
111
|
+
return manifest;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function writeScaffoldPackageJson({ destDir, projectName }) {
|
|
115
|
+
const selfPkg = readSelfPackage();
|
|
116
|
+
const devkitName = typeof selfPkg?.name === 'string' && selfPkg.name.trim() ? selfPkg.name.trim() : '@leeoohoo/ui-apps-devkit';
|
|
117
|
+
const devkitVersion = typeof selfPkg?.version === 'string' && selfPkg.version.trim() ? selfPkg.version.trim() : '0.1.0';
|
|
118
|
+
const devkitRange = `^${devkitVersion}`;
|
|
119
|
+
|
|
120
|
+
const baseScripts = {
|
|
121
|
+
dev: 'chatos-uiapp dev',
|
|
122
|
+
validate: 'chatos-uiapp validate',
|
|
123
|
+
pack: 'chatos-uiapp pack',
|
|
124
|
+
'install:chatos': 'chatos-uiapp install --host-app chatos',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const pkgPath = path.join(destDir, 'package.json');
|
|
128
|
+
const existing = isFile(pkgPath) ? readJson(pkgPath) : {};
|
|
129
|
+
const pkg = existing && typeof existing === 'object' ? existing : {};
|
|
130
|
+
|
|
131
|
+
pkg.name = projectName;
|
|
132
|
+
pkg.private = true;
|
|
133
|
+
pkg.type = 'module';
|
|
134
|
+
|
|
135
|
+
pkg.scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
136
|
+
for (const [key, value] of Object.entries(baseScripts)) {
|
|
137
|
+
if (!pkg.scripts[key]) pkg.scripts[key] = value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pkg.devDependencies = pkg.devDependencies && typeof pkg.devDependencies === 'object' ? pkg.devDependencies : {};
|
|
141
|
+
if (!pkg.devDependencies[devkitName]) {
|
|
142
|
+
pkg.devDependencies[devkitName] = devkitRange;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeJson(pkgPath, pkg);
|
|
146
|
+
return pkg;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function writeScaffoldConfig({ destDir, pluginDir = 'plugin', appId = '' }) {
|
|
150
|
+
const cfgPath = path.join(destDir, 'chatos.config.json');
|
|
151
|
+
const existing = isFile(cfgPath) ? readJson(cfgPath) : {};
|
|
152
|
+
const cfg = existing && typeof existing === 'object' ? existing : {};
|
|
153
|
+
cfg.pluginDir = pluginDir;
|
|
154
|
+
cfg.appId = appId;
|
|
155
|
+
writeJson(cfgPath, cfg);
|
|
156
|
+
return cfg;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function maybeReplaceTokensInFile(filePath, replacements) {
|
|
160
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
161
|
+
let next = raw;
|
|
162
|
+
for (const [key, value] of Object.entries(replacements || {})) {
|
|
163
|
+
next = next.split(key).join(String(value));
|
|
164
|
+
}
|
|
165
|
+
if (next !== raw) {
|
|
166
|
+
writeText(filePath, next);
|
|
167
|
+
}
|
|
168
|
+
}
|