@leeoohoo/ui-apps-devkit 0.1.2 → 0.1.3

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.
Files changed (61) hide show
  1. package/README.md +61 -62
  2. package/bin/chatos-uiapp.js +3 -4
  3. package/package.json +23 -23
  4. package/src/cli.js +53 -53
  5. package/src/commands/dev.js +14 -14
  6. package/src/commands/init.js +129 -129
  7. package/src/commands/install.js +45 -45
  8. package/src/commands/pack.js +72 -72
  9. package/src/commands/validate.js +90 -138
  10. package/src/lib/args.js +49 -49
  11. package/src/lib/config.js +29 -29
  12. package/src/lib/fs.js +78 -78
  13. package/src/lib/path-boundary.js +16 -16
  14. package/src/lib/plugin.js +45 -45
  15. package/src/lib/template.js +172 -172
  16. package/src/sandbox/server.js +1204 -1028
  17. package/templates/basic/README.md +63 -65
  18. package/templates/basic/chatos.config.json +5 -5
  19. package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
  20. package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
  21. package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  22. package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
  23. package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
  24. package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  25. package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
  26. package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  27. package/templates/basic/plugin/apps/app/compact.mjs +41 -41
  28. package/templates/basic/plugin/apps/app/index.mjs +287 -287
  29. package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -7
  30. package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -7
  31. package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -15
  32. package/templates/basic/plugin/backend/index.mjs +37 -37
  33. package/templates/basic/template.json +7 -7
  34. package/templates/notepad/README.md +38 -44
  35. package/templates/notepad/chatos.config.json +4 -4
  36. package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +209 -211
  37. package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +73 -73
  38. package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  39. package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +106 -106
  40. package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +239 -239
  41. package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  42. package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +40 -40
  43. package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  44. package/templates/notepad/plugin/apps/app/api.mjs +30 -30
  45. package/templates/notepad/plugin/apps/app/compact.mjs +41 -41
  46. package/templates/notepad/plugin/apps/app/dom.mjs +14 -14
  47. package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -35
  48. package/templates/notepad/plugin/apps/app/index.mjs +1056 -1056
  49. package/templates/notepad/plugin/apps/app/layers.mjs +338 -338
  50. package/templates/notepad/plugin/apps/app/markdown.mjs +120 -120
  51. package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -22
  52. package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -22
  53. package/templates/notepad/plugin/apps/app/mcp-server.mjs +199 -199
  54. package/templates/notepad/plugin/apps/app/styles.mjs +355 -355
  55. package/templates/notepad/plugin/apps/app/tags.mjs +21 -21
  56. package/templates/notepad/plugin/apps/app/ui.mjs +280 -280
  57. package/templates/notepad/plugin/backend/index.mjs +99 -99
  58. package/templates/notepad/plugin/plugin.json +23 -23
  59. package/templates/notepad/plugin/shared/notepad-paths.mjs +39 -39
  60. package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
  61. package/templates/notepad/template.json +8 -8
@@ -5,52 +5,52 @@ import { copyDir, ensureDir, isDirectory, rmForce, sanitizeDirComponent } from '
5
5
  import { loadDevkitConfig } from '../lib/config.js';
6
6
  import { findPluginDir, loadPluginManifest } from '../lib/plugin.js';
7
7
  import { STATE_ROOT_DIRNAME } from '../lib/state-constants.js';
8
-
8
+
9
9
  function defaultStateDir(hostApp) {
10
10
  const app = typeof hostApp === 'string' && hostApp.trim() ? hostApp.trim() : 'chatos';
11
11
  return path.join(os.homedir(), STATE_ROOT_DIRNAME, app);
12
12
  }
13
-
14
- function copyPluginDir(srcDir, destDir) {
15
- ensureDir(path.dirname(destDir));
16
- rmForce(destDir);
17
- copyDir(srcDir, destDir, {
18
- filter: (src) => {
19
- const base = path.basename(src);
20
- if (base === 'node_modules') return false;
21
- if (base === '.git') return false;
22
- if (base === '.DS_Store') return false;
23
- if (base.endsWith('.map')) return false;
24
- return true;
25
- },
26
- });
27
- }
28
-
29
- export async function cmdInstall({ flags }) {
30
- const { config } = loadDevkitConfig(process.cwd());
31
- const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
32
- const { pluginId, name, version } = loadPluginManifest(pluginDir);
33
-
34
- const hostApp = String(flags['host-app'] || flags.hostApp || 'chatos').trim() || 'chatos';
35
- const stateDir = String(flags['state-dir'] || flags.stateDir || defaultStateDir(hostApp)).trim();
36
- if (!stateDir) throw new Error('stateDir is required');
37
-
38
- const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
39
- ensureDir(pluginsRoot);
40
-
41
- const dirName = sanitizeDirComponent(pluginId);
42
- if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
43
-
44
- const destDir = path.join(pluginsRoot, dirName);
45
- const replaced = isDirectory(destDir);
46
- copyPluginDir(pluginDir, destDir);
47
-
48
- // eslint-disable-next-line no-console
49
- console.log(
50
- `Installed: ${pluginId} (${name}@${version})\n` +
51
- ` -> ${destDir}\n` +
52
- ` replaced: ${replaced}\n\n` +
53
- `Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
54
- );
55
- }
56
-
13
+
14
+ function copyPluginDir(srcDir, destDir) {
15
+ ensureDir(path.dirname(destDir));
16
+ rmForce(destDir);
17
+ copyDir(srcDir, destDir, {
18
+ filter: (src) => {
19
+ const base = path.basename(src);
20
+ if (base === 'node_modules') return false;
21
+ if (base === '.git') return false;
22
+ if (base === '.DS_Store') return false;
23
+ if (base.endsWith('.map')) return false;
24
+ return true;
25
+ },
26
+ });
27
+ }
28
+
29
+ export async function cmdInstall({ flags }) {
30
+ const { config } = loadDevkitConfig(process.cwd());
31
+ const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
32
+ const { pluginId, name, version } = loadPluginManifest(pluginDir);
33
+
34
+ const hostApp = String(flags['host-app'] || flags.hostApp || 'chatos').trim() || 'chatos';
35
+ const stateDir = String(flags['state-dir'] || flags.stateDir || defaultStateDir(hostApp)).trim();
36
+ if (!stateDir) throw new Error('stateDir is required');
37
+
38
+ const pluginsRoot = path.join(stateDir, 'ui_apps', 'plugins');
39
+ ensureDir(pluginsRoot);
40
+
41
+ const dirName = sanitizeDirComponent(pluginId);
42
+ if (!dirName) throw new Error(`Invalid plugin id: ${pluginId}`);
43
+
44
+ const destDir = path.join(pluginsRoot, dirName);
45
+ const replaced = isDirectory(destDir);
46
+ copyPluginDir(pluginDir, destDir);
47
+
48
+ // eslint-disable-next-line no-console
49
+ console.log(
50
+ `Installed: ${pluginId} (${name}@${version})\n` +
51
+ ` -> ${destDir}\n` +
52
+ ` replaced: ${replaced}\n\n` +
53
+ `Open ChatOS -> 应用 -> 刷新(同 id 覆盖生效)。`
54
+ );
55
+ }
56
+
@@ -1,72 +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
- }
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
+ }
@@ -1,15 +1,15 @@
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
-
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
13
  function statSizeSafe(filePath) {
14
14
  try {
15
15
  return fs.statSync(filePath).size;
@@ -18,142 +18,94 @@ function statSizeSafe(filePath) {
18
18
  }
19
19
  }
20
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
21
  export async function cmdValidate({ flags }) {
55
22
  const { config } = loadDevkitConfig(process.cwd());
56
23
  const pluginDir = findPluginDir(process.cwd(), flags['plugin-dir'] || flags.pluginDir || config?.pluginDir);
57
24
 
58
25
  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
26
 
68
27
  const manifestSize = statSizeSafe(manifestPath);
69
28
  assert(manifestSize <= 256 * 1024, `plugin.json too large (>256KiB): ${manifestSize} bytes`);
70
-
71
- assert(Number(manifest?.manifestVersion || 1) === 1, 'manifestVersion must be 1');
72
- assert(typeof manifest?.id === 'string' && manifest.id.trim(), 'plugin.id is required');
73
- assert(typeof manifest?.name === 'string' && manifest.name.trim(), 'plugin.name is required');
74
-
75
- if (manifest?.backend?.entry) {
76
- const backendAbs = resolveInsideDir(pluginDir, manifest.backend.entry);
77
- assert(isFile(backendAbs), `backend.entry must be a file: ${manifest.backend.entry}`);
78
- }
79
-
80
- const apps = Array.isArray(manifest?.apps) ? manifest.apps : [];
81
- assert(apps.length > 0, 'plugin.apps[] is required (>=1)');
82
- const ids = new Set();
83
- for (const app of apps) {
84
- const appId = typeof app?.id === 'string' ? app.id.trim() : '';
85
- assert(appId, 'apps[i].id is required');
86
- assert(!ids.has(appId), `duplicate app.id: ${appId}`);
87
- ids.add(appId);
88
-
89
- assert(typeof app?.name === 'string' && app.name.trim(), `apps[${appId}].name is required`);
90
- assert(app?.entry?.type === 'module', `apps[${appId}].entry.type must be "module"`);
91
- const entryPath = typeof app?.entry?.path === 'string' ? app.entry.path.trim() : '';
92
- assert(entryPath, `apps[${appId}].entry.path is required`);
93
- const entryAbs = resolveInsideDir(pluginDir, entryPath);
94
- assert(isFile(entryAbs), `apps[${appId}].entry.path must be a file: ${entryPath}`);
95
-
96
- const compactType = app?.entry?.compact?.type;
97
- if (compactType && compactType !== 'module') {
98
- assert(false, `apps[${appId}].entry.compact.type must be "module"`);
99
- }
100
- const compactPath = typeof app?.entry?.compact?.path === 'string' ? app.entry.compact.path.trim() : '';
101
- if (compactPath) {
102
- const compactAbs = resolveInsideDir(pluginDir, compactPath);
103
- assert(isFile(compactAbs), `apps[${appId}].entry.compact.path must be a file: ${compactPath}`);
104
- }
105
-
106
- // Basic ai path boundary checks (full schema is defined in docs; this validates the security boundary).
107
- const ai = app?.ai;
108
- const aiObj =
109
- typeof ai === 'string'
110
- ? { config: ai }
111
- : ai && typeof ai === 'object'
112
- ? ai
113
- : null;
114
-
115
- if (aiObj?.config) {
116
- const abs = resolveInsideDir(pluginDir, aiObj.config);
117
- assert(isFile(abs), `apps[${appId}].ai.config must be a file: ${aiObj.config}`);
118
- const size = statSizeSafe(abs);
119
- assert(size <= 128 * 1024, `ai.config too large (>128KiB): ${aiObj.config}`);
120
- }
121
-
122
- if (aiObj?.mcp?.entry) {
123
- const abs = resolveInsideDir(pluginDir, aiObj.mcp.entry);
124
- assert(isFile(abs), `apps[${appId}].ai.mcp.entry must be a file: ${aiObj.mcp.entry}`);
125
- }
126
-
127
- const mcpPrompt = aiObj?.mcpPrompt;
128
- const collectPromptPaths = () => {
129
- if (!mcpPrompt) return [];
130
- if (typeof mcpPrompt === 'string') return [mcpPrompt];
131
- if (typeof mcpPrompt !== 'object') return [];
132
- const zh = mcpPrompt.zh;
133
- const en = mcpPrompt.en;
134
- const out = [];
135
- const pushSource = (src) => {
136
- if (!src) return;
137
- if (typeof src === 'string') out.push(src);
138
- else if (typeof src === 'object' && typeof src.path === 'string') out.push(src.path);
139
- };
140
- pushSource(zh);
141
- pushSource(en);
142
- return out;
143
- };
144
- for (const rel of collectPromptPaths()) {
145
- const abs = resolveInsideDir(pluginDir, rel);
146
- assert(isFile(abs), `apps[${appId}].ai.mcpPrompt path must be a file: ${rel}`);
147
- const size = statSizeSafe(abs);
148
- assert(size <= 128 * 1024, `mcpPrompt too large (>128KiB): ${rel}`);
149
- }
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}`);
150
37
  }
151
38
 
152
- if (warnings.length > 0) {
153
- warnings.forEach((message) => {
154
- // eslint-disable-next-line no-console
155
- console.warn(`WARN: ${message}`);
156
- });
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
+ const compactType = app?.entry?.compact?.type;
56
+ if (compactType && compactType !== 'module') {
57
+ assert(false, `apps[${appId}].entry.compact.type must be "module"`);
58
+ }
59
+ const compactPath = typeof app?.entry?.compact?.path === 'string' ? app.entry.compact.path.trim() : '';
60
+ if (compactPath) {
61
+ const compactAbs = resolveInsideDir(pluginDir, compactPath);
62
+ assert(isFile(compactAbs), `apps[${appId}].entry.compact.path must be a file: ${compactPath}`);
63
+ }
64
+
65
+ // Basic ai path boundary checks (full schema is defined in docs; this validates the security boundary).
66
+ const ai = app?.ai;
67
+ const aiObj =
68
+ typeof ai === 'string'
69
+ ? { config: ai }
70
+ : ai && typeof ai === 'object'
71
+ ? ai
72
+ : null;
73
+
74
+ if (aiObj?.config) {
75
+ const abs = resolveInsideDir(pluginDir, aiObj.config);
76
+ assert(isFile(abs), `apps[${appId}].ai.config must be a file: ${aiObj.config}`);
77
+ const size = statSizeSafe(abs);
78
+ assert(size <= 128 * 1024, `ai.config too large (>128KiB): ${aiObj.config}`);
79
+ }
80
+
81
+ if (aiObj?.mcp?.entry) {
82
+ const abs = resolveInsideDir(pluginDir, aiObj.mcp.entry);
83
+ assert(isFile(abs), `apps[${appId}].ai.mcp.entry must be a file: ${aiObj.mcp.entry}`);
84
+ }
85
+
86
+ const mcpPrompt = aiObj?.mcpPrompt;
87
+ const collectPromptPaths = () => {
88
+ if (!mcpPrompt) return [];
89
+ if (typeof mcpPrompt === 'string') return [mcpPrompt];
90
+ if (typeof mcpPrompt !== 'object') return [];
91
+ const zh = mcpPrompt.zh;
92
+ const en = mcpPrompt.en;
93
+ const out = [];
94
+ const pushSource = (src) => {
95
+ if (!src) return;
96
+ if (typeof src === 'string') out.push(src);
97
+ else if (typeof src === 'object' && typeof src.path === 'string') out.push(src.path);
98
+ };
99
+ pushSource(zh);
100
+ pushSource(en);
101
+ return out;
102
+ };
103
+ for (const rel of collectPromptPaths()) {
104
+ const abs = resolveInsideDir(pluginDir, rel);
105
+ assert(isFile(abs), `apps[${appId}].ai.mcpPrompt path must be a file: ${rel}`);
106
+ const size = statSizeSafe(abs);
107
+ assert(size <= 128 * 1024, `mcpPrompt too large (>128KiB): ${rel}`);
108
+ }
157
109
  }
158
110
 
159
111
  // eslint-disable-next-line no-console
package/src/lib/args.js CHANGED
@@ -1,49 +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
-
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
+
package/src/lib/config.js CHANGED
@@ -1,29 +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
-
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
+