@qomicex/cli 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.
@@ -0,0 +1,126 @@
1
+ // 极简 ZIP 读写(无依赖):node:zlib deflateRaw + 手写 CRC32/目录结构。
2
+ // 仅覆盖 .qplugin 需要的子集:无加密、无目录条目、UTF-8 文件名、deflate/存储。
3
+ import { deflateRawSync, inflateRawSync } from 'node:zlib';
4
+ const CRC_TABLE = (() => {
5
+ const table = new Uint32Array(256);
6
+ for (let n = 0; n < 256; n++) {
7
+ let c = n;
8
+ for (let k = 0; k < 8; k++)
9
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
10
+ table[n] = c >>> 0;
11
+ }
12
+ return table;
13
+ })();
14
+ export function crc32(data) {
15
+ let crc = 0xffffffff;
16
+ for (let i = 0; i < data.length; i++) {
17
+ crc = CRC_TABLE[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
18
+ }
19
+ return (crc ^ 0xffffffff) >>> 0;
20
+ }
21
+ function u16(n) {
22
+ return new Uint8Array([n & 0xff, (n >>> 8) & 0xff]);
23
+ }
24
+ function u32(n) {
25
+ return new Uint8Array([n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]);
26
+ }
27
+ function concat(parts) {
28
+ const total = parts.reduce((s, p) => s + p.length, 0);
29
+ const out = new Uint8Array(total);
30
+ let off = 0;
31
+ for (const p of parts) {
32
+ out.set(p, off);
33
+ off += p.length;
34
+ }
35
+ return out;
36
+ }
37
+ export function zipWrite(files) {
38
+ const locals = [];
39
+ const central = [];
40
+ let offset = 0;
41
+ const UTF8_FLAG = 0x0800;
42
+ for (const file of files) {
43
+ const nameBytes = new TextEncoder().encode(file.name);
44
+ const comp = deflateRawSync(file.data);
45
+ const crc = crc32(file.data);
46
+ const method = file.data.length === 0 ? 0 : 8;
47
+ locals.push(u32(0x04034b50), u16(20), // version needed
48
+ u16(UTF8_FLAG), // flags
49
+ u16(method), u16(0), u16(0), // mod time/date
50
+ u32(crc), u32(comp.length), u32(file.data.length), u16(nameBytes.length), u16(0), // extra len
51
+ nameBytes, method === 8 ? comp : file.data);
52
+ central.push(u32(0x02014b50), u16(0x0314), // version made by (unix)
53
+ u16(20), u16(UTF8_FLAG), u16(method), u16(0), u16(0), u32(crc), u32(comp.length), u32(file.data.length), u16(nameBytes.length), u16(0), u16(0), // extra / comment len
54
+ u16(0), u16(0), // disk start / internal attrs
55
+ u32(0), // external attrs
56
+ u32(offset), nameBytes);
57
+ offset += 30 + nameBytes.length + (method === 8 ? comp.length : file.data.length);
58
+ }
59
+ const cdBytes = concat(central);
60
+ const cdOffset = offset;
61
+ const eocd = concat([
62
+ u32(0x06054b50),
63
+ u16(0), u16(0),
64
+ u16(files.length), u16(files.length),
65
+ u32(cdBytes.length),
66
+ u32(cdOffset),
67
+ u16(0),
68
+ ]);
69
+ return concat([...locals, cdBytes, eocd]);
70
+ }
71
+ function findEocd(bytes) {
72
+ const maxBack = Math.min(bytes.length, 65557);
73
+ for (let i = bytes.length - 22; i >= bytes.length - maxBack; i--) {
74
+ if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
75
+ return i;
76
+ }
77
+ }
78
+ throw new Error('不是有效的 zip 包(缺少 EOCD)');
79
+ }
80
+ function readCentral(bytes) {
81
+ const eocd = findEocd(bytes);
82
+ const count = bytes[eocd + 10] | (bytes[eocd + 11] << 8);
83
+ const cdOffset = bytes[eocd + 16] | (bytes[eocd + 17] << 8) | (bytes[eocd + 18] << 16) | (bytes[eocd + 19] << 24);
84
+ const entries = [];
85
+ let p = cdOffset;
86
+ for (let i = 0; i < count; i++) {
87
+ if (bytes[p] !== 0x50 || bytes[p + 1] !== 0x4b || bytes[p + 2] !== 0x01 || bytes[p + 3] !== 0x02) {
88
+ throw new Error('zip 中央目录解析失败');
89
+ }
90
+ const method = bytes[p + 10] | (bytes[p + 11] << 8);
91
+ const compSize = bytes[p + 20] | (bytes[p + 21] << 8) | (bytes[p + 22] << 16) | (bytes[p + 23] << 24);
92
+ const size = bytes[p + 24] | (bytes[p + 25] << 8) | (bytes[p + 26] << 16) | (bytes[p + 27] << 24);
93
+ const nameLen = bytes[p + 28] | (bytes[p + 29] << 8);
94
+ const extraLen = bytes[p + 30] | (bytes[p + 31] << 8);
95
+ const commentLen = bytes[p + 32] | (bytes[p + 33] << 8);
96
+ const localOffset = bytes[p + 42] | (bytes[p + 43] << 8) | (bytes[p + 44] << 16) | (bytes[p + 45] << 24);
97
+ const name = new TextDecoder().decode(bytes.subarray(p + 46, p + 46 + nameLen));
98
+ entries.push({ name, method, compSize, size, localOffset });
99
+ p += 46 + nameLen + extraLen + commentLen;
100
+ }
101
+ return entries;
102
+ }
103
+ export function zipRead(bytes) {
104
+ const out = {};
105
+ for (const e of readCentral(bytes)) {
106
+ if (e.name.endsWith('/'))
107
+ continue;
108
+ const p = e.localOffset;
109
+ const nameLen = bytes[p + 26] | (bytes[p + 27] << 8);
110
+ const extraLen = bytes[p + 28] | (bytes[p + 29] << 8);
111
+ const dataStart = p + 30 + nameLen + extraLen;
112
+ const raw = bytes.subarray(dataStart, dataStart + e.compSize);
113
+ let data;
114
+ if (e.method === 0) {
115
+ data = raw;
116
+ }
117
+ else if (e.method === 8) {
118
+ data = inflateRawSync(raw);
119
+ }
120
+ else {
121
+ throw new Error(`不支持的 zip 压缩方式: ${e.method} (${e.name})`);
122
+ }
123
+ out[e.name] = data;
124
+ }
125
+ return out;
126
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@qomicex/cli",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "description": "Qomicex 插件生态 CLI:create / dev / pack / verify / publish",
9
+ "bin": {
10
+ "qomicex": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "templates"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "typescript": "~5.8.3"
26
+ }
27
+ }
@@ -0,0 +1,20 @@
1
+ # __QOMICEX_PLUGIN_NAME__
2
+
3
+ 由 `qomicex create __QOMICEX_PLUGIN_ID__` 生成的最小插件模板(Vite + React 19 + TypeScript + Tailwind + `@qomicex/plugin-ui`)。
4
+
5
+ ## 开发
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm run dev # Vite 热重载
10
+ ```
11
+
12
+ ## 打包与校验
13
+
14
+ ```bash
15
+ qomicex verify # manifest 合法性 + 权限最小化 + 长循环告警
16
+ qomicex pack # tsc && vite build → 打 .qplugin
17
+ qomicex publish # 设备流登录后签名并上传到商店
18
+ ```
19
+
20
+ 详见 `packages/qomicex-cli/README.md`。
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__QOMICEX_PLUGIN_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "__QOMICEX_PLUGIN_ID__",
3
+ "name": "__QOMICEX_PLUGIN_NAME__",
4
+ "version": "0.1.0",
5
+ "minLauncherVersion": "0.1.0",
6
+ "layers": ["l2"],
7
+ "permissions": ["config:read", "ui:toast", "network:cors_proxy"],
8
+ "entry": {
9
+ "frontend": "dist/index.html",
10
+ "theme": "dist/theme.css"
11
+ },
12
+ "contributes": {
13
+ "menuItems": [
14
+ {
15
+ "path": "/plugins/p/__QOMICEX_PLUGIN_ID__",
16
+ "label": "__QOMICEX_PLUGIN_NAME__",
17
+ "icon": "🧩",
18
+ "action": "page"
19
+ }
20
+ ]
21
+ }
22
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "__QOMICEX_PLUGIN_ID__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build"
9
+ },
10
+ "dependencies": {
11
+ "@qomicex/plugin-ui": "^0.2.1",
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^19.0.0",
17
+ "@types/react-dom": "^19.0.0",
18
+ "@vitejs/plugin-react": "^4.7.0",
19
+ "autoprefixer": "^10.4.20",
20
+ "postcss": "^8.4.49",
21
+ "tailwindcss": "^3.4.19",
22
+ "typescript": "~5.8.3",
23
+ "vite": "^7.3.6"
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ # 生成项目自成一 workspace root:可独立 pnpm install,且不受外层仓库 workspace 影响。
2
+ # pnpm 11 用 allowBuilds 允许 esbuild 构建脚本(Linux/mac 需其 postinstall 下载二进制)。
3
+ allowBuilds:
4
+ esbuild: true
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,55 @@
1
+ import { useCallback, useState } from 'react'
2
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@qomicex/plugin-ui'
3
+ import { getApi, getPluginId } from './api.ts'
4
+
5
+ export default function App() {
6
+ const api = getApi()
7
+ const id = getPluginId()
8
+ const [log, setLog] = useState('')
9
+
10
+ const run = useCallback(async (label: string, fn: () => Promise<unknown>) => {
11
+ try {
12
+ const res = await fn()
13
+ setLog(`✓ ${label}\n${JSON.stringify(res, null, 2)}`)
14
+ } catch (e) {
15
+ setLog(`✗ ${label}\n${e instanceof Error ? e.message : String(e)}`)
16
+ }
17
+ }, [])
18
+
19
+ const loadSetting = useCallback(() => {
20
+ run('getSettings', () => api!.call('getSettings', id) as Promise<unknown>)
21
+ }, [api, id, run])
22
+
23
+ return (
24
+ <Card className="m-4">
25
+ <CardHeader>
26
+ <CardTitle>你好,{id}</CardTitle>
27
+ <CardDescription>
28
+ 插件 API 桥已{api ? '注入' : '未注入(浏览器直开时优雅降级)'}。
29
+ </CardDescription>
30
+ </CardHeader>
31
+ <CardContent className="flex flex-col gap-2">
32
+ <div className="flex gap-2">
33
+ <Button onClick={loadSetting}>读取配置(config:read)</Button>
34
+ <Button
35
+ variant="outline"
36
+ onClick={() => run('showToast', () => api!.call('showToast', '来自插件的提示', 'info'))}
37
+ >
38
+ 弹提示(ui:toast)
39
+ </Button>
40
+ <Button
41
+ variant="outline"
42
+ onClick={() =>
43
+ run('proxyFetch', () =>
44
+ api!.call('proxyFetch', { url: 'https://api.qomicex.top/ping', method: 'GET' }) as Promise<unknown>
45
+ )
46
+ }
47
+ >
48
+ 网络请求(network:fetch)
49
+ </Button>
50
+ </div>
51
+ {log && <pre className="whitespace-pre-wrap rounded bg-muted p-3 text-xs text-muted-foreground">{log}</pre>}
52
+ </CardContent>
53
+ </Card>
54
+ )
55
+ }
@@ -0,0 +1,35 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare global {
4
+ interface Window {
5
+ __PLUGIN_API__?: PluginApi
6
+ __PLUGIN_ID__?: string
7
+ }
8
+ }
9
+
10
+ export interface ProxyRequest {
11
+ url: string
12
+ method?: string
13
+ headers?: Record<string, string>
14
+ body?: string
15
+ timeoutMs?: number
16
+ }
17
+
18
+ export interface PluginApi {
19
+ call: (method: string, ...args: unknown[]) => Promise<unknown>
20
+ registerMethod: (method: string, fn: (...args: unknown[]) => unknown) => void
21
+ callPlugin: (pluginId: string, method: string, ...args: unknown[]) => Promise<unknown>
22
+ proxyFetchStream: (req: ProxyRequest, handlers: { onChunk: (c: string) => void; onError: (e: Error) => void }) => Promise<void>
23
+ }
24
+
25
+ /**
26
+ * 沙箱注入的插件 API 桥;在 l2 iframe / inline 渲染中均可用。
27
+ * 独立 `pnpm dev`(浏览器直开)时返回 null,UI 可优雅降级。
28
+ */
29
+ export function getApi(): PluginApi | null {
30
+ return window.__PLUGIN_API__ ?? null
31
+ }
32
+
33
+ export function getPluginId(): string {
34
+ return window.__PLUGIN_ID__ ?? 'unknown'
35
+ }
@@ -0,0 +1,9 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html,
6
+ body,
7
+ #root {
8
+ height: 100%;
9
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import './index.css'
5
+
6
+ const rootEl = document.getElementById('root')
7
+ if (rootEl) createRoot(rootEl).render(<React.StrictMode><App /></React.StrictMode>)
@@ -0,0 +1,8 @@
1
+ import preset from '@qomicex/plugin-ui/tailwind-preset'
2
+
3
+ /** @type {import('tailwindcss').Config} */
4
+ export default {
5
+ content: ['./index.html', './src/**/*.{ts,tsx}', './node_modules/@qomicex/plugin-ui/dist/**/*.{js,ts,tsx}'],
6
+ presets: [preset],
7
+ darkMode: 'class',
8
+ }
@@ -0,0 +1,7 @@
1
+ /* 插件主题:只消费主题语义 token(见 docs/junsi-dev-docs/2-架构设计/主题语义Token规范v1.md)。
2
+ v1 以 CSS 变量覆盖为主,全部 var() 引用,禁止内联色值。 */
3
+ :root[data-theme] {
4
+ /* 可选:覆盖默认 HSL token 例如
5
+ --primary: 142 71% 48%;
6
+ */
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler",
8
+ "allowImportingTsExtensions": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "jsx": "react-jsx",
13
+ "strict": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true
17
+ },
18
+ "include": ["src"]
19
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ // 关键:必须用相对路径 base,否则产物中的 /assets/... 会被当成站点根路径
7
+ base: './',
8
+ build: {
9
+ outDir: 'dist',
10
+ rollupOptions: {
11
+ input: {
12
+ main: 'index.html',
13
+ },
14
+ },
15
+ },
16
+ })