@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.
- package/README.md +86 -0
- package/dist/commands/create.js +74 -0
- package/dist/commands/dev.js +59 -0
- package/dist/commands/pack.js +85 -0
- package/dist/commands/publish.js +126 -0
- package/dist/commands/verify.js +80 -0
- package/dist/index.js +111 -0
- package/dist/lib/io.js +35 -0
- package/dist/lib/manifest.js +93 -0
- package/dist/lib/permissions.js +51 -0
- package/dist/lib/project.js +35 -0
- package/dist/lib/scan.js +104 -0
- package/dist/lib/signature.js +171 -0
- package/dist/lib/store.js +100 -0
- package/dist/lib/zip.js +126 -0
- package/package.json +27 -0
- package/templates/plugin/README.md +20 -0
- package/templates/plugin/index.html +12 -0
- package/templates/plugin/manifest.json +22 -0
- package/templates/plugin/package.json +25 -0
- package/templates/plugin/pnpm-workspace.yaml +4 -0
- package/templates/plugin/postcss.config.js +6 -0
- package/templates/plugin/src/App.tsx +55 -0
- package/templates/plugin/src/api.ts +35 -0
- package/templates/plugin/src/index.css +9 -0
- package/templates/plugin/src/main.tsx +7 -0
- package/templates/plugin/tailwind.config.js +8 -0
- package/templates/plugin/theme.css +7 -0
- package/templates/plugin/tsconfig.json +19 -0
- package/templates/plugin/vite.config.ts +16 -0
package/dist/lib/zip.js
ADDED
|
@@ -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,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,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,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
|
+
})
|