ais6-app 0.1.0 → 0.2.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 +10 -1
- package/README.zh-CN.md +9 -1
- package/bin/ais6-app.mjs +30 -1
- package/package.json +5 -3
- package/src/create.mjs +64 -0
- package/src/index.mjs +1 -0
- package/template/LICENSE +21 -0
- package/template/README.md +40 -0
- package/template/README.zh-CN.md +37 -0
- package/template/_gitignore +4 -0
- package/template/biome.json +8 -0
- package/template/index.html +14 -0
- package/template/package.json +27 -0
- package/template/public/ais6-bridge.js +711 -0
- package/template/public/app.manifest.json +34 -0
- package/template/public/kit.css +431 -0
- package/template/src/bridge/bridge.ts +89 -0
- package/template/src/bridge/types.ts +100 -0
- package/template/src/main.tsx +105 -0
- package/template/src/strings/en.ts +15 -0
- package/template/src/strings/index.ts +23 -0
- package/template/src/strings/zh.ts +13 -0
- package/template/src/styles/app.css +42 -0
- package/template/src/ui/App.tsx +34 -0
- package/template/tests/unit/strings.test.ts +36 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.config.ts +9 -0
- package/template/vitest.config.ts +5 -0
package/README.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# ais6-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create an AI System 6 app, and check it the way a desk checks it before
|
|
4
|
+
installing it.
|
|
4
5
|
|
|
5
6
|
```
|
|
7
|
+
npx ais6-app create my-app # a new TypeScript + Preact app
|
|
6
8
|
npx ais6-app check # the app folder you are in
|
|
7
9
|
npx ais6-app check path/to/app --build
|
|
8
10
|
```
|
|
9
11
|
|
|
12
|
+
`create` writes a working app into an empty folder: a typed bridge client,
|
|
13
|
+
`public/app.manifest.json` with one window and a File menu, the kit files,
|
|
14
|
+
strings in English and Chinese, Vitest tests, Biome, a README pair and an MIT
|
|
15
|
+
licence. Options: `--id` (default: the folder name), `--name`, `--name-zh`,
|
|
16
|
+
`--author`. `npm run check` in the new folder runs types, lint, tests, the
|
|
17
|
+
build and `ais6-app check`; it passes before you change anything.
|
|
18
|
+
|
|
10
19
|
It runs on a repository (source and build), on a built `dist/`, or on a
|
|
11
20
|
vendored app tree, and prints one line per check: `OK`, `NO` (a desk would
|
|
12
21
|
refuse the app) or `--` (a warning). The exit code is 0 when everything
|
package/README.zh-CN.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
<!-- canonical-source: packages/ais6-app/README.md -->
|
|
2
|
+
<!-- source-sha256: 246087a9156aa7044435339b6056107420709aba7633d78c2507709caddb1ab1 -->
|
|
3
|
+
|
|
4
|
+
> 英文版为准;本说明仅供人类参考。
|
|
5
|
+
|
|
1
6
|
# ais6-app
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
新建 AI System 6 app,并按桌面在安装前的检查方式检查它。
|
|
4
9
|
|
|
5
10
|
```
|
|
11
|
+
npx ais6-app create my-app # 新建一个 TypeScript + Preact app
|
|
6
12
|
npx ais6-app check # 当前所在的 app 文件夹
|
|
7
13
|
npx ais6-app check path/to/app --build
|
|
8
14
|
```
|
|
9
15
|
|
|
16
|
+
`create` 在空文件夹里写出一个能运行的 app:带类型的桥接客户端、只有一个窗口和「文件」菜单的 `public/app.manifest.json`、kit 文件、中英文字符串、Vitest 测试、Biome、README 中英两份和 MIT 许可证。选项:`--id`(默认用文件夹名)、`--name`、`--name-zh`、`--author`。在新文件夹里运行 `npm run check` 会依次跑类型、lint、测试、构建和 `ais6-app check`;什么都不改也能通过。
|
|
17
|
+
|
|
10
18
|
可以对仓库(源码与构建)、构建好的 `dist/` 或已托管的 app 树运行,每项检查打印一行:`OK`、`NO`(桌面会拒绝这个 app)或 `--`(警告)。全部通过时退出码为 0,有项目被拒为 1,用法错误为 2。
|
|
11
19
|
|
|
12
20
|
| 检查 | 看什么 |
|
package/bin/ais6-app.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// ais6-app — check an AI System 6 app the way a desk checks it.
|
|
3
3
|
//
|
|
4
4
|
// ais6-app check [dir] [--build] [--json] [--own-storage idbfs] [--engine-files a.js,b.js]
|
|
5
|
+
// ais6-app create <dir> [--id my-app] [--name "My App"] [--name-zh 我的应用] [--author "Name"]
|
|
5
6
|
//
|
|
6
7
|
// Exit code 0 when every check passes, 1 when one fails, 2 on a usage error.
|
|
7
8
|
|
|
8
9
|
import { readFileSync } from "node:fs";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import { runCheck } from "../src/check.mjs";
|
|
12
|
+
import { createApp } from "../src/create.mjs";
|
|
11
13
|
|
|
12
14
|
const args = process.argv.slice(2);
|
|
13
15
|
const command = args[0];
|
|
@@ -18,6 +20,8 @@ if (!command || command === "--help" || command === "-h" || command === "help")
|
|
|
18
20
|
|
|
19
21
|
Usage:
|
|
20
22
|
ais6-app check [dir] [--build] [--json] check an app folder (default: .)
|
|
23
|
+
ais6-app create <dir> [--id] [--name] [--name-zh] [--author]
|
|
24
|
+
a new TypeScript + Preact app
|
|
21
25
|
|
|
22
26
|
--build run "npm run build" first and check the built dist/
|
|
23
27
|
--json print the result as JSON
|
|
@@ -33,9 +37,34 @@ private files). Docs: https://aisystem6.aaronlau.me/sdk/`);
|
|
|
33
37
|
process.exit(command ? 0 : 2);
|
|
34
38
|
}
|
|
35
39
|
if (command === "--version" || command === "-v") { console.log(version); process.exit(0); }
|
|
40
|
+
const valueOf = (flag) => { const index = args.indexOf(flag); return index >= 0 ? String(args[index + 1] || "") : ""; };
|
|
41
|
+
|
|
42
|
+
if (command === "create") {
|
|
43
|
+
const named = new Set(["--id", "--name", "--name-zh", "--author"].map((flag) => valueOf(flag)).filter(Boolean));
|
|
44
|
+
const target = args.slice(1).find((arg) => !arg.startsWith("--") && !named.has(arg));
|
|
45
|
+
if (!target) { console.error("ais6-app create needs a folder: ais6-app create my-app"); process.exit(2); }
|
|
46
|
+
try {
|
|
47
|
+
const made = createApp(target, {
|
|
48
|
+
id: valueOf("--id") || undefined,
|
|
49
|
+
name: valueOf("--name") || undefined,
|
|
50
|
+
nameZh: valueOf("--name-zh") || undefined,
|
|
51
|
+
author: valueOf("--author") || undefined,
|
|
52
|
+
});
|
|
53
|
+
console.log(`Created ${made.id} (window ${made.window}) in ${made.dir}, ${made.files.length} files.
|
|
54
|
+
|
|
55
|
+
Next:
|
|
56
|
+
cd ${target}
|
|
57
|
+
npm install
|
|
58
|
+
npm run dev # the app on its own page
|
|
59
|
+
npm run check # types, lint, tests, build and ais6-app check`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(error.message);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
36
66
|
if (command !== "check") { console.error(`unknown command "${command}"; try ais6-app --help`); process.exit(2); }
|
|
37
67
|
|
|
38
|
-
const valueOf = (flag) => { const index = args.indexOf(flag); return index >= 0 ? String(args[index + 1] || "") : ""; };
|
|
39
68
|
const listOf = (flag) => valueOf(flag).split(",").map((part) => part.trim()).filter(Boolean);
|
|
40
69
|
const flagValues = new Set(["--own-storage", "--engine-files"].map((flag) => valueOf(flag)).filter(Boolean));
|
|
41
70
|
const dir = resolve(args.slice(1).find((arg) => !arg.startsWith("--") && !flagValues.has(arg)) || ".");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ais6-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Create an AI System 6 app, and check it the way a desk checks it before installing it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
".": "./src/index.mjs",
|
|
12
12
|
"./manifest": "./src/manifest.mjs",
|
|
13
13
|
"./boundary": "./src/boundary.mjs",
|
|
14
|
-
"./publish": "./src/publish.mjs"
|
|
14
|
+
"./publish": "./src/publish.mjs",
|
|
15
|
+
"./create": "./src/create.mjs"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"bin",
|
|
18
19
|
"src",
|
|
20
|
+
"template",
|
|
19
21
|
"README.md",
|
|
20
22
|
"README.zh-CN.md",
|
|
21
23
|
"LICENSE"
|
package/src/create.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// `ais6-app create <dir>`: a new app from the TypeScript + Preact template,
|
|
2
|
+
// ready to build and to pass `ais6-app check` before any change.
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
6
|
+
import { ID_PATTERN } from "./manifest.mjs";
|
|
7
|
+
|
|
8
|
+
const TEMPLATE = new URL("../template/", import.meta.url);
|
|
9
|
+
// npm drops a published .gitignore, so the template ships it under another name.
|
|
10
|
+
const RENAMES = { _gitignore: ".gitignore" };
|
|
11
|
+
|
|
12
|
+
/** "word-count" → "wordCount": the window and app name the desk addresses. */
|
|
13
|
+
export function windowNameFor(id) {
|
|
14
|
+
return id.replace(/-([a-z0-9])/g, (match, next) => next.toUpperCase());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** "word-count" → "Word Count". */
|
|
18
|
+
export function titleFor(id) {
|
|
19
|
+
return id.split("-").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function walk(dir, out = []) {
|
|
23
|
+
for (const name of readdirSync(dir).sort()) {
|
|
24
|
+
const path = join(dir, name);
|
|
25
|
+
if (statSync(path).isDirectory()) walk(path, out);
|
|
26
|
+
else out.push(path);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Writes the template into `dir`. Returns { dir, id, window, files }.
|
|
33
|
+
* Throws with a plain sentence when the id is not one a desk accepts or the
|
|
34
|
+
* folder is not empty.
|
|
35
|
+
*/
|
|
36
|
+
export function createApp(target, { id, name, nameZh, author = "The authors", year = new Date().getFullYear() } = {}) {
|
|
37
|
+
const dir = resolve(target);
|
|
38
|
+
const appId = id || basename(dir);
|
|
39
|
+
if (!ID_PATTERN.test(appId)) throw new Error(`"${appId}" is not an app id a desk accepts: lower case letters, digits and dashes, starting with a letter (pass --id)`);
|
|
40
|
+
if (existsSync(dir) && readdirSync(dir).length) throw new Error(`${dir} is not empty; choose a new folder`);
|
|
41
|
+
const title = name || titleFor(appId);
|
|
42
|
+
const values = {
|
|
43
|
+
__APP_ID__: appId,
|
|
44
|
+
__WINDOW__: windowNameFor(appId),
|
|
45
|
+
__NAME__: title,
|
|
46
|
+
__NAME_ZH__: nameZh || title,
|
|
47
|
+
__AUTHOR__: author,
|
|
48
|
+
__YEAR__: String(year),
|
|
49
|
+
__CHECKER_VERSION__: JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version,
|
|
50
|
+
};
|
|
51
|
+
const root = TEMPLATE.pathname ? decodeURIComponent(TEMPLATE.pathname) : String(TEMPLATE);
|
|
52
|
+
const files = [];
|
|
53
|
+
for (const source of walk(root)) {
|
|
54
|
+
const rel = relative(root, source).split("/").map((part) => RENAMES[part] || part).join("/");
|
|
55
|
+
const out = join(dir, rel);
|
|
56
|
+
mkdirSync(join(out, ".."), { recursive: true });
|
|
57
|
+
const text = readFileSync(source, "utf8");
|
|
58
|
+
// The kit files are copied byte for byte; everything else gets the app's names.
|
|
59
|
+
const filled = /(^|\/)public\/(ais6-bridge\.js|kit\.css)$/.test(rel) ? text : text.replace(/__[A-Z_]+__/g, (key) => values[key] ?? key);
|
|
60
|
+
writeFileSync(out, filled);
|
|
61
|
+
files.push(rel);
|
|
62
|
+
}
|
|
63
|
+
return { dir, id: appId, window: values.__WINDOW__, files };
|
|
64
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -2,3 +2,4 @@ export { runCheck, locate } from "./check.mjs";
|
|
|
2
2
|
export { manifestProblems, allowProblems, kitRangeAllows, CAPABILITIES, FRAME_PERMISSIONS, PROTOCOLS, KIT_MAJOR, ID_PATTERN } from "./manifest.mjs";
|
|
3
3
|
export { scanAppTree, APP_SIDE_RULES, OWN_STORAGE } from "./boundary.mjs";
|
|
4
4
|
export { scanForPublishing, credentialPatterns, localPathPatterns, forbiddenPathPatterns } from "./publish.mjs";
|
|
5
|
+
export { createApp, windowNameFor, titleFor } from "./create.mjs";
|
package/template/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) __YEAR__ __AUTHOR__
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
An app for AI System 6, made from the `ais6-app create` template
|
|
4
|
+
(TypeScript + Preact). It runs in a window of the desk, and on its own page.
|
|
5
|
+
|
|
6
|
+
[中文说明](README.zh-CN.md)
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev # its own page, with the kit's standalone adapter
|
|
13
|
+
npm run check # types, lint, tests, build, then ais6-app check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`npm run build` writes `dist/`, the folder a desk installs.
|
|
17
|
+
|
|
18
|
+
## Where things are
|
|
19
|
+
|
|
20
|
+
| Path | What it holds |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `public/app.manifest.json` | id, version, window, capabilities, menus, the window title in both languages |
|
|
23
|
+
| `public/ais6-bridge.js`, `public/kit.css` | the app kit: the bridge client and the shared controls. Do not edit; update them from the kit |
|
|
24
|
+
| `src/bridge/` | the one connection to the desk, typed |
|
|
25
|
+
| `src/strings/` | every string, in English and Chinese; a missing key does not compile |
|
|
26
|
+
| `src/ui/`, `src/styles/` | the window's content, drawn with the kit's classes and tokens |
|
|
27
|
+
|
|
28
|
+
## Rules a desk enforces
|
|
29
|
+
|
|
30
|
+
- Everything outside the frame goes through the bridge: no `window.parent`,
|
|
31
|
+
no desk globals, no `localStorage` of your own.
|
|
32
|
+
- The version in `app.manifest.json`, `package.json` and `APP_VERSION` in
|
|
33
|
+
`src/bridge/bridge.ts` is one number. Raise all three together.
|
|
34
|
+
- A capability or service the manifest does not list is refused.
|
|
35
|
+
|
|
36
|
+
The full SDK: https://aisystem6.aaronlau.me/sdk/
|
|
37
|
+
|
|
38
|
+
## Licence
|
|
39
|
+
|
|
40
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# __NAME_ZH__
|
|
2
|
+
|
|
3
|
+
一个 AI System 6 应用,由 `ais6-app create` 模板生成(TypeScript + Preact)。它在桌面的窗口里运行,也能在自己的页面上单独运行。
|
|
4
|
+
|
|
5
|
+
[English](README.md)
|
|
6
|
+
|
|
7
|
+
## 运行
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev # 独立页面,由套件的独立适配器应答
|
|
12
|
+
npm run check # 类型、lint、测试、构建,最后跑 ais6-app check
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`npm run build` 生成 `dist/`,也就是桌面安装的文件夹。
|
|
16
|
+
|
|
17
|
+
## 文件在哪里
|
|
18
|
+
|
|
19
|
+
| 路径 | 内容 |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `public/app.manifest.json` | id、版本、窗口、能力、菜单,以及两种语言的窗口标题 |
|
|
22
|
+
| `public/ais6-bridge.js`、`public/kit.css` | 应用套件:桥接客户端和共用控件。不要改,从套件更新 |
|
|
23
|
+
| `src/bridge/` | 与桌面之间唯一的连接,带类型 |
|
|
24
|
+
| `src/strings/` | 所有文字,英文和中文各一份;少一个键就编译不过 |
|
|
25
|
+
| `src/ui/`、`src/styles/` | 窗口内容,用套件的类和令牌绘制 |
|
|
26
|
+
|
|
27
|
+
## 桌面会检查的规则
|
|
28
|
+
|
|
29
|
+
- 框架之外的一切都经过桥接:不碰 `window.parent`,不碰桌面的全局对象,不自己用 `localStorage`。
|
|
30
|
+
- `app.manifest.json`、`package.json` 和 `src/bridge/bridge.ts` 里的 `APP_VERSION` 是同一个版本号,要一起改。
|
|
31
|
+
- 清单没列出的能力或服务一律拒绝。
|
|
32
|
+
|
|
33
|
+
完整 SDK:https://aisystem6.aaronlau.me/sdk/
|
|
34
|
+
|
|
35
|
+
## 许可
|
|
36
|
+
|
|
37
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"includes": ["**", "!dist", "!node_modules", "!public", "!**/*.md"]
|
|
5
|
+
},
|
|
6
|
+
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 },
|
|
7
|
+
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>__NAME__</title>
|
|
7
|
+
<link rel="stylesheet" href="./kit.css" />
|
|
8
|
+
<script src="./ais6-bridge.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<script type="module" src="./src/main.tsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_ID__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc -b && vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"typecheck": "tsc -b",
|
|
12
|
+
"lint": "biome check .",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"check": "npm run typecheck && npm run lint && npm run test && npm run build && ais6-app check ."
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"preact": "11.2.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "2.6.7",
|
|
21
|
+
"@preact/preset-vite": "2.10.6",
|
|
22
|
+
"ais6-app": "^__CHECKER_VERSION__",
|
|
23
|
+
"typescript": "7.0.2",
|
|
24
|
+
"vite": "8.3.8",
|
|
25
|
+
"vitest": "5.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|