@limecloud/agent-app-studio 0.1.0 → 0.1.1
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/APP.md +68 -0
- package/README.md +1 -1
- package/app/app.js +24 -8
- package/app/index.html +1 -1
- package/app.capabilities.yaml +22 -0
- package/dist/ui/app.js +43 -0
- package/dist/ui/index.html +41 -0
- package/dist/ui/styles.css +129 -0
- package/dist/ui/vendor/.gitkeep +0 -0
- package/package.json +7 -1
- package/scripts/build-agent-app.mjs +14 -0
- package/src/core/config.mjs +2 -3
- package/src/server.mjs +24 -3
package/APP.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
manifestVersion: 0.7.0
|
|
3
|
+
name: lime-agent-app-studio
|
|
4
|
+
displayName: Lime Agent App Studio
|
|
5
|
+
version: 0.1.1
|
|
6
|
+
status: preview
|
|
7
|
+
appType: developer-tool
|
|
8
|
+
description: 面向已认证开发者的 Agent App 可视化发布工作台和 npm CLI 入口。
|
|
9
|
+
runtimeTargets:
|
|
10
|
+
- local
|
|
11
|
+
requires:
|
|
12
|
+
lime:
|
|
13
|
+
appRuntime: ">=0.7.0 <1.0.0"
|
|
14
|
+
sdk: "@lime/app-sdk@^0.7.0"
|
|
15
|
+
capabilities:
|
|
16
|
+
- lime.ui
|
|
17
|
+
- lime.files
|
|
18
|
+
- lime.agent
|
|
19
|
+
- lime.evidence
|
|
20
|
+
categories:
|
|
21
|
+
- developer
|
|
22
|
+
- developer_only
|
|
23
|
+
- tools
|
|
24
|
+
publisher:
|
|
25
|
+
publisherId: lime-cloud
|
|
26
|
+
name: Lime Cloud
|
|
27
|
+
displayName: Lime Cloud
|
|
28
|
+
kind: platform
|
|
29
|
+
verified: true
|
|
30
|
+
distribution:
|
|
31
|
+
channel: developer-preview
|
|
32
|
+
visibility: developer_only
|
|
33
|
+
pricing: included
|
|
34
|
+
billingModel: none
|
|
35
|
+
runtimePackage:
|
|
36
|
+
ui:
|
|
37
|
+
path: ./dist/ui
|
|
38
|
+
entries:
|
|
39
|
+
- key: dashboard
|
|
40
|
+
kind: page
|
|
41
|
+
title: 发布工作台
|
|
42
|
+
route: /dashboard
|
|
43
|
+
- key: cli_quickstart
|
|
44
|
+
kind: page
|
|
45
|
+
title: CLI 快速开始
|
|
46
|
+
route: /cli
|
|
47
|
+
quickstart:
|
|
48
|
+
entry: dashboard
|
|
49
|
+
setupSteps:
|
|
50
|
+
- complete_developer_certification
|
|
51
|
+
- install_npm_cli
|
|
52
|
+
- run_publish_dry_run
|
|
53
|
+
support:
|
|
54
|
+
url: ./docs/v1/README.md
|
|
55
|
+
license: Apache-2.0
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# Lime Agent App Studio
|
|
59
|
+
|
|
60
|
+
Lime Agent App Studio 是开发者工具入口。已认证开发者可以在 Lime 应用中心安装它,并通过可视化工作台或 npm CLI 将 Agent App 打包、Dry-run 和发布到 LimeCore 云端。
|
|
61
|
+
|
|
62
|
+
## CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g @limecloud/agent-app-studio
|
|
66
|
+
lime-agent-app-studio auth status --tenant-id tenant-0001
|
|
67
|
+
lime-agent-app-studio publish --app-dir ./my-agent-app --channel beta --dry-run
|
|
68
|
+
```
|
package/README.md
CHANGED
|
@@ -17,5 +17,5 @@ lime-agent-app-studio studio --port 4177
|
|
|
17
17
|
|
|
18
18
|
环境变量:
|
|
19
19
|
|
|
20
|
-
- `LIMECORE_API_BASE_URL`:LimeCore API base,默认 `https://api.
|
|
20
|
+
- `LIMECORE_API_BASE_URL`:LimeCore API base,默认 `https://lime-api.limeai.run/api`
|
|
21
21
|
- `LIME_AGENT_APP_STUDIO_TOKEN`:开发者登录 token,CI/CD 推荐使用
|
package/app/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fields = ["appDir", "appId", "tenantId", "apiBase", "token", "channel"];
|
|
2
2
|
const output = document.querySelector("#output");
|
|
3
3
|
const statusEl = document.querySelector("#status");
|
|
4
|
+
const defaultApiBase = "https://lime-api.limeai.run/api";
|
|
4
5
|
|
|
5
6
|
function values() {
|
|
6
7
|
return Object.fromEntries(fields.map((id) => [id, document.querySelector(`#${id}`).value.trim()]).filter(([, value]) => value));
|
|
@@ -9,16 +10,31 @@ function values() {
|
|
|
9
10
|
async function post(path, body) {
|
|
10
11
|
statusEl.textContent = "执行中";
|
|
11
12
|
output.textContent = "请稍候...";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(path, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
});
|
|
19
|
+
const payload = await response.json();
|
|
20
|
+
statusEl.textContent = response.ok ? "完成" : "失败";
|
|
21
|
+
output.textContent = JSON.stringify(payload, null, 2);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
statusEl.textContent = "需要本地服务";
|
|
24
|
+
output.textContent = [
|
|
25
|
+
"当前页面没有连接到本地 Studio 服务。",
|
|
26
|
+
"",
|
|
27
|
+
"如果你是在 Lime 应用中心里打开:",
|
|
28
|
+
"1. 先安装 CLI:npm install -g @limecloud/agent-app-studio",
|
|
29
|
+
"2. 启动本地可视化服务:lime-agent-app-studio studio --port 4177",
|
|
30
|
+
`3. 默认 API Base:${defaultApiBase}`,
|
|
31
|
+
"",
|
|
32
|
+
`错误:${error?.message || String(error)}`,
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
20
35
|
}
|
|
21
36
|
|
|
37
|
+
document.querySelector("#apiBase").placeholder = defaultApiBase;
|
|
22
38
|
document.querySelector("#inspectBtn").addEventListener("click", () => post("/api/inspect", values()));
|
|
23
39
|
document.querySelector("#dryRunBtn").addEventListener("click", () => post("/api/publish", { ...values(), dryRun: true }));
|
|
24
40
|
document.querySelector("#publishBtn").addEventListener("click", () => {
|
package/app/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<label>App 目录 <input id="appDir" value="." /></label>
|
|
19
19
|
<label>App ID <input id="appId" placeholder="自动识别或手动填写" /></label>
|
|
20
20
|
<label>Tenant ID <input id="tenantId" placeholder="tenant-0001" /></label>
|
|
21
|
-
<label>API Base <input id="apiBase" placeholder="https://api.
|
|
21
|
+
<label>API Base <input id="apiBase" placeholder="https://lime-api.limeai.run/api" /></label>
|
|
22
22
|
<label>Token <input id="token" type="password" placeholder="开发者 token" /></label>
|
|
23
23
|
<label>Channel <input id="channel" value="beta" /></label>
|
|
24
24
|
<div class="actions">
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Lime Agent App Studio 能力声明。
|
|
2
|
+
capabilities:
|
|
3
|
+
lime.ui:
|
|
4
|
+
features:
|
|
5
|
+
- pages
|
|
6
|
+
- developer-console
|
|
7
|
+
routes:
|
|
8
|
+
- path: /dashboard
|
|
9
|
+
component: ./dist/ui/index.html
|
|
10
|
+
title: 发布工作台
|
|
11
|
+
- path: /cli
|
|
12
|
+
component: ./dist/ui/index.html
|
|
13
|
+
title: CLI 快速开始
|
|
14
|
+
lime.files:
|
|
15
|
+
required: false
|
|
16
|
+
reason: 选择本地 Agent App 目录用于诊断和打包。
|
|
17
|
+
lime.agent:
|
|
18
|
+
required: false
|
|
19
|
+
reason: 后续版本用于生成发布前检查和修复建议。
|
|
20
|
+
lime.evidence:
|
|
21
|
+
required: false
|
|
22
|
+
reason: 记录 dry-run 与发布审计证据。
|
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fields = ["appDir", "appId", "tenantId", "apiBase", "token", "channel"];
|
|
2
|
+
const output = document.querySelector("#output");
|
|
3
|
+
const statusEl = document.querySelector("#status");
|
|
4
|
+
const defaultApiBase = "https://lime-api.limeai.run/api";
|
|
5
|
+
|
|
6
|
+
function values() {
|
|
7
|
+
return Object.fromEntries(fields.map((id) => [id, document.querySelector(`#${id}`).value.trim()]).filter(([, value]) => value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function post(path, body) {
|
|
11
|
+
statusEl.textContent = "执行中";
|
|
12
|
+
output.textContent = "请稍候...";
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(path, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
});
|
|
19
|
+
const payload = await response.json();
|
|
20
|
+
statusEl.textContent = response.ok ? "完成" : "失败";
|
|
21
|
+
output.textContent = JSON.stringify(payload, null, 2);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
statusEl.textContent = "需要本地服务";
|
|
24
|
+
output.textContent = [
|
|
25
|
+
"当前页面没有连接到本地 Studio 服务。",
|
|
26
|
+
"",
|
|
27
|
+
"如果你是在 Lime 应用中心里打开:",
|
|
28
|
+
"1. 先安装 CLI:npm install -g @limecloud/agent-app-studio",
|
|
29
|
+
"2. 启动本地可视化服务:lime-agent-app-studio studio --port 4177",
|
|
30
|
+
`3. 默认 API Base:${defaultApiBase}`,
|
|
31
|
+
"",
|
|
32
|
+
`错误:${error?.message || String(error)}`,
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
document.querySelector("#apiBase").placeholder = defaultApiBase;
|
|
38
|
+
document.querySelector("#inspectBtn").addEventListener("click", () => post("/api/inspect", values()));
|
|
39
|
+
document.querySelector("#dryRunBtn").addEventListener("click", () => post("/api/publish", { ...values(), dryRun: true }));
|
|
40
|
+
document.querySelector("#publishBtn").addEventListener("click", () => {
|
|
41
|
+
if (!confirm("正式发布会写入 LimeCore 云端 Release,确认继续?")) return;
|
|
42
|
+
post("/api/publish", { ...values(), publish: true });
|
|
43
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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" />
|
|
6
|
+
<title>Lime Agent App Studio</title>
|
|
7
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main class="shell">
|
|
11
|
+
<section class="hero">
|
|
12
|
+
<p class="eyebrow">Lime Developer Tool</p>
|
|
13
|
+
<h1>Agent App 发布工作台</h1>
|
|
14
|
+
<p>诊断本地 Agent App,预演发布计划,并在通过开发者认证后上传到 LimeCore 云端 Release。</p>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<section class="panel form-panel">
|
|
18
|
+
<label>App 目录 <input id="appDir" value="." /></label>
|
|
19
|
+
<label>App ID <input id="appId" placeholder="自动识别或手动填写" /></label>
|
|
20
|
+
<label>Tenant ID <input id="tenantId" placeholder="tenant-0001" /></label>
|
|
21
|
+
<label>API Base <input id="apiBase" placeholder="https://lime-api.limeai.run/api" /></label>
|
|
22
|
+
<label>Token <input id="token" type="password" placeholder="开发者 token" /></label>
|
|
23
|
+
<label>Channel <input id="channel" value="beta" /></label>
|
|
24
|
+
<div class="actions">
|
|
25
|
+
<button id="inspectBtn">诊断项目</button>
|
|
26
|
+
<button id="dryRunBtn">Dry-run</button>
|
|
27
|
+
<button id="publishBtn" class="danger">正式发布</button>
|
|
28
|
+
</div>
|
|
29
|
+
</section>
|
|
30
|
+
|
|
31
|
+
<section class="panel">
|
|
32
|
+
<div class="panel-head">
|
|
33
|
+
<h2>结果</h2>
|
|
34
|
+
<span id="status">等待操作</span>
|
|
35
|
+
</div>
|
|
36
|
+
<pre id="output">选择本地 Agent App 目录后开始。</pre>
|
|
37
|
+
</section>
|
|
38
|
+
</main>
|
|
39
|
+
<script type="module" src="/app.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--ink: #112036;
|
|
3
|
+
--muted: #66745f;
|
|
4
|
+
--card: rgba(255, 252, 244, 0.88);
|
|
5
|
+
--line: #dfd5bf;
|
|
6
|
+
--green: #2f8a54;
|
|
7
|
+
--blue: #14265c;
|
|
8
|
+
--danger: #9a4a27;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
color: var(--ink);
|
|
16
|
+
font-family: Charter, Georgia, "Times New Roman", serif;
|
|
17
|
+
background:
|
|
18
|
+
radial-gradient(circle at 10% 10%, rgba(47, 138, 84, 0.20), transparent 28rem),
|
|
19
|
+
radial-gradient(circle at 90% 20%, rgba(20, 38, 92, 0.16), transparent 32rem),
|
|
20
|
+
linear-gradient(135deg, #fffaf0, #f4efe1 60%, #e9efdc);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.shell {
|
|
24
|
+
width: min(1120px, calc(100vw - 40px));
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
padding: 48px 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.hero {
|
|
30
|
+
display: grid;
|
|
31
|
+
gap: 12px;
|
|
32
|
+
max-width: 760px;
|
|
33
|
+
margin-bottom: 28px;
|
|
34
|
+
}
|
|
35
|
+
.eyebrow {
|
|
36
|
+
margin: 0;
|
|
37
|
+
color: var(--green);
|
|
38
|
+
font-weight: 800;
|
|
39
|
+
letter-spacing: 0.12em;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
}
|
|
42
|
+
h1 {
|
|
43
|
+
margin: 0;
|
|
44
|
+
font-size: clamp(40px, 7vw, 88px);
|
|
45
|
+
line-height: 0.92;
|
|
46
|
+
}
|
|
47
|
+
.hero p:last-child {
|
|
48
|
+
margin: 0;
|
|
49
|
+
color: var(--muted);
|
|
50
|
+
font-size: 20px;
|
|
51
|
+
line-height: 1.6;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.panel {
|
|
55
|
+
margin-top: 18px;
|
|
56
|
+
padding: 24px;
|
|
57
|
+
border: 1px solid var(--line);
|
|
58
|
+
border-radius: 28px;
|
|
59
|
+
background: var(--card);
|
|
60
|
+
box-shadow: 0 22px 70px rgba(60, 49, 25, 0.14);
|
|
61
|
+
backdrop-filter: blur(18px);
|
|
62
|
+
}
|
|
63
|
+
.form-panel {
|
|
64
|
+
display: grid;
|
|
65
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
66
|
+
gap: 16px;
|
|
67
|
+
}
|
|
68
|
+
label {
|
|
69
|
+
display: grid;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
color: var(--muted);
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
}
|
|
74
|
+
input {
|
|
75
|
+
width: 100%;
|
|
76
|
+
border: 1px solid var(--line);
|
|
77
|
+
border-radius: 16px;
|
|
78
|
+
padding: 13px 14px;
|
|
79
|
+
color: var(--ink);
|
|
80
|
+
background: rgba(255, 255, 255, 0.72);
|
|
81
|
+
font: 600 15px ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
82
|
+
}
|
|
83
|
+
.actions {
|
|
84
|
+
grid-column: 1 / -1;
|
|
85
|
+
display: flex;
|
|
86
|
+
gap: 12px;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
}
|
|
89
|
+
button {
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: 999px;
|
|
92
|
+
padding: 13px 22px;
|
|
93
|
+
color: white;
|
|
94
|
+
background: var(--blue);
|
|
95
|
+
font-weight: 800;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
button:nth-child(1) { background: var(--green); }
|
|
99
|
+
button.danger { background: var(--danger); }
|
|
100
|
+
.panel-head {
|
|
101
|
+
display: flex;
|
|
102
|
+
justify-content: space-between;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 16px;
|
|
105
|
+
}
|
|
106
|
+
h2 { margin: 0; }
|
|
107
|
+
#status {
|
|
108
|
+
border: 1px solid var(--line);
|
|
109
|
+
border-radius: 999px;
|
|
110
|
+
padding: 8px 12px;
|
|
111
|
+
color: var(--muted);
|
|
112
|
+
background: rgba(255,255,255,0.72);
|
|
113
|
+
font-weight: 800;
|
|
114
|
+
}
|
|
115
|
+
pre {
|
|
116
|
+
overflow: auto;
|
|
117
|
+
min-height: 280px;
|
|
118
|
+
margin: 18px 0 0;
|
|
119
|
+
padding: 18px;
|
|
120
|
+
border-radius: 20px;
|
|
121
|
+
background: #101827;
|
|
122
|
+
color: #e9f2dc;
|
|
123
|
+
font-size: 13px;
|
|
124
|
+
line-height: 1.6;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@media (max-width: 760px) {
|
|
128
|
+
.form-panel { grid-template-columns: 1fr; }
|
|
129
|
+
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limecloud/agent-app-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Lime Agent App Studio CLI and visual publisher for Agent App packages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -13,11 +13,17 @@
|
|
|
13
13
|
"bin",
|
|
14
14
|
"src",
|
|
15
15
|
"app",
|
|
16
|
+
"dist",
|
|
17
|
+
"scripts",
|
|
18
|
+
"APP.md",
|
|
19
|
+
"app.capabilities.yaml",
|
|
16
20
|
"docs/v1/README.md",
|
|
17
21
|
"LICENSE",
|
|
18
22
|
"README.md"
|
|
19
23
|
],
|
|
20
24
|
"scripts": {
|
|
25
|
+
"build": "node scripts/build-agent-app.mjs",
|
|
26
|
+
"validate:app": "node src/cli.mjs project inspect --app-dir .",
|
|
21
27
|
"test": "node --test tests/*.test.mjs",
|
|
22
28
|
"pack:dry-run": "npm pack --dry-run",
|
|
23
29
|
"publish:dry-run": "npm publish --access public --dry-run",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
6
|
+
const distUi = join(root, "dist", "ui");
|
|
7
|
+
|
|
8
|
+
await rm(distUi, { recursive: true, force: true });
|
|
9
|
+
await mkdir(join(distUi, "vendor"), { recursive: true });
|
|
10
|
+
for (const file of ["index.html", "app.js", "styles.css"]) {
|
|
11
|
+
await cp(join(root, "app", file), join(distUi, file));
|
|
12
|
+
}
|
|
13
|
+
await writeFile(join(distUi, "vendor", ".gitkeep"), "");
|
|
14
|
+
console.log(`已生成 Agent App UI: ${distUi}`);
|
package/src/core/config.mjs
CHANGED
|
@@ -7,11 +7,10 @@ import { homedir } from "node:os";
|
|
|
7
7
|
|
|
8
8
|
const defaultConfigDir = join(homedir(), ".lime", "agent-app-studio");
|
|
9
9
|
const defaultConfigPath = join(defaultConfigDir, "config.json");
|
|
10
|
+
const defaultApiBase = "https://lime-api.limeai.run/api";
|
|
10
11
|
|
|
11
12
|
export function resolveApiBase(options = {}) {
|
|
12
|
-
return trimTrailingSlash(
|
|
13
|
-
options.apiBase || process.env.LIMECORE_API_BASE_URL || "https://api.limecloud.run/api"
|
|
14
|
-
);
|
|
13
|
+
return trimTrailingSlash(options.apiBase || process.env.LIMECORE_API_BASE_URL || defaultApiBase);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export async function loadStudioConfig() {
|
package/src/server.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const root = fileURLToPath(new URL("..", import.meta.url));
|
|
|
13
13
|
const appRoot = join(root, "app");
|
|
14
14
|
|
|
15
15
|
export async function startStudioServer(options = {}) {
|
|
16
|
-
const port = Number(options.port
|
|
16
|
+
const port = Number(options.port ?? 4177);
|
|
17
17
|
const server = createServer(async (req, res) => {
|
|
18
18
|
try {
|
|
19
19
|
if (req.method === "POST" && req.url === "/api/inspect") {
|
|
@@ -31,14 +31,25 @@ export async function startStudioServer(options = {}) {
|
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
await new Promise((resolve) => server.listen(port, resolve));
|
|
34
|
-
|
|
34
|
+
const address = server.address();
|
|
35
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
36
|
+
return { server, url: `http://127.0.0.1:${actualPort}` };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
async function serveStatic(req, res) {
|
|
38
40
|
const pathname = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
|
41
|
+
if (pathname === "/favicon.ico") return sendNoContent(res);
|
|
39
42
|
const safePath = pathname.replace(/\.\./g, "");
|
|
40
43
|
const filePath = join(appRoot, safePath);
|
|
41
|
-
|
|
44
|
+
let content;
|
|
45
|
+
try {
|
|
46
|
+
content = await readFile(filePath);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.code === "ENOENT" || error?.code === "EISDIR") {
|
|
49
|
+
return sendNotFound(res);
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
42
53
|
const type = contentType(filePath);
|
|
43
54
|
res.writeHead(200, { "Content-Type": type });
|
|
44
55
|
res.end(content);
|
|
@@ -56,6 +67,16 @@ function sendJson(res, payload, status = 200) {
|
|
|
56
67
|
res.end(JSON.stringify(payload, null, 2));
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
function sendNoContent(res) {
|
|
71
|
+
res.writeHead(204, { "Content-Length": "0" });
|
|
72
|
+
res.end();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sendNotFound(res) {
|
|
76
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
77
|
+
res.end("Not Found");
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
function contentType(path) {
|
|
60
81
|
switch (extname(path)) {
|
|
61
82
|
case ".js":
|