@leeoohoo/ui-apps-devkit 0.1.1 → 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 (62) hide show
  1. package/README.md +84 -76
  2. package/bin/chatos-uiapp.js +3 -4
  3. package/package.json +28 -25
  4. package/src/cli.js +53 -53
  5. package/src/commands/dev.js +14 -14
  6. package/src/commands/init.js +143 -142
  7. package/src/commands/install.js +56 -55
  8. package/src/commands/pack.js +72 -72
  9. package/src/commands/validate.js +113 -113
  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/state-constants.js +2 -0
  16. package/src/lib/template.js +172 -172
  17. package/src/sandbox/server.js +2302 -1200
  18. package/templates/basic/README.md +80 -77
  19. package/templates/basic/chatos.config.json +5 -5
  20. package/templates/basic/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +178 -178
  21. package/templates/basic/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +75 -74
  22. package/templates/basic/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  23. package/templates/basic/docs/CHATOS_UI_APPS_OVERVIEW.md +115 -113
  24. package/templates/basic/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +225 -224
  25. package/templates/basic/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  26. package/templates/basic/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -45
  27. package/templates/basic/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  28. package/templates/basic/plugin/apps/app/compact.mjs +41 -41
  29. package/templates/basic/plugin/apps/app/index.mjs +287 -287
  30. package/templates/basic/plugin/apps/app/mcp-prompt.en.md +7 -7
  31. package/templates/basic/plugin/apps/app/mcp-prompt.zh.md +7 -7
  32. package/templates/basic/plugin/apps/app/mcp-server.mjs +15 -15
  33. package/templates/basic/plugin/backend/index.mjs +37 -37
  34. package/templates/basic/template.json +7 -7
  35. package/templates/notepad/README.md +61 -58
  36. package/templates/notepad/chatos.config.json +4 -4
  37. package/templates/notepad/docs/CHATOS_UI_APPS_AI_CONTRIBUTIONS.md +178 -178
  38. package/templates/notepad/docs/CHATOS_UI_APPS_BACKEND_PROTOCOL.md +75 -74
  39. package/templates/notepad/docs/CHATOS_UI_APPS_HOST_API.md +136 -136
  40. package/templates/notepad/docs/CHATOS_UI_APPS_OVERVIEW.md +115 -113
  41. package/templates/notepad/docs/CHATOS_UI_APPS_PLUGIN_MANIFEST.md +225 -224
  42. package/templates/notepad/docs/CHATOS_UI_APPS_STYLE_GUIDE.md +95 -95
  43. package/templates/notepad/docs/CHATOS_UI_APPS_TROUBLESHOOTING.md +45 -45
  44. package/templates/notepad/docs/CHATOS_UI_PROMPTS_PROTOCOL.md +392 -392
  45. package/templates/notepad/plugin/apps/app/api.mjs +30 -30
  46. package/templates/notepad/plugin/apps/app/compact.mjs +41 -41
  47. package/templates/notepad/plugin/apps/app/dom.mjs +14 -14
  48. package/templates/notepad/plugin/apps/app/ds-tree.mjs +35 -35
  49. package/templates/notepad/plugin/apps/app/index.mjs +1056 -1056
  50. package/templates/notepad/plugin/apps/app/layers.mjs +338 -338
  51. package/templates/notepad/plugin/apps/app/markdown.mjs +120 -120
  52. package/templates/notepad/plugin/apps/app/mcp-prompt.en.md +22 -22
  53. package/templates/notepad/plugin/apps/app/mcp-prompt.zh.md +22 -22
  54. package/templates/notepad/plugin/apps/app/mcp-server.mjs +207 -200
  55. package/templates/notepad/plugin/apps/app/styles.mjs +355 -355
  56. package/templates/notepad/plugin/apps/app/tags.mjs +21 -21
  57. package/templates/notepad/plugin/apps/app/ui.mjs +280 -280
  58. package/templates/notepad/plugin/backend/index.mjs +99 -99
  59. package/templates/notepad/plugin/plugin.json +23 -23
  60. package/templates/notepad/plugin/shared/notepad-paths.mjs +80 -62
  61. package/templates/notepad/plugin/shared/notepad-store.mjs +765 -765
  62. package/templates/notepad/template.json +8 -8
@@ -1,113 +1,113 @@
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
- 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
- }
109
- }
110
-
111
- // eslint-disable-next-line no-console
112
- console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
113
- }
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
+ 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
+ }
109
+ }
110
+
111
+ // eslint-disable-next-line no-console
112
+ console.log(`OK: ${path.relative(process.cwd(), pluginDir)} (apps=${apps.length})`);
113
+ }
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
+
package/src/lib/fs.js CHANGED
@@ -1,78 +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
-
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
+
@@ -1,16 +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
-
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
+
package/src/lib/plugin.js CHANGED
@@ -1,45 +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
-
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
+