openclacky 1.3.9 → 1.3.11
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/lib/clacky/brand_config.rb +32 -6
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +47 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +167 -80
- data/lib/clacky/extension/packager.rb +2 -3
- data/lib/clacky/locales/en.rb +1 -1
- data/lib/clacky/locales/zh.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/server/http_server.rb +119 -13
- data/lib/clacky/telemetry.rb +9 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +99 -14
- data/lib/clacky/web/features/extensions/store.js +34 -9
- data/lib/clacky/web/features/extensions/view.js +47 -11
- data/lib/clacky/web/features/new-session/view.js +1 -1
- data/lib/clacky/web/i18n.js +18 -6
- data/lib/clacky/web/sessions.js +32 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eccc2d78824c13072991199a00edd5b848216334422ce37876cd639854abb4ab
|
|
4
|
+
data.tar.gz: c29754fc48b5b9638168fa167c5335c32ccf5c64a3db94d3e64ac2638581857e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d2e6ef2dcf07a50779241a3ed5b384c2fbf34e4265208874567b12d1280e29f2fe79124286ab7b73251bfc836de01ebf9d8ad03f91ae12ad55ac4575128bfd3
|
|
7
|
+
data.tar.gz: 77088322e7cecc8c0cc0f7138786263c3de14dff2d1b33e99cee763437918e226bd34f0ef318a12d4f7292bcad5a935a8ca2ca16c9a15d8d2bd7262eebee9ccd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.3.11] - 2026-07-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Delete button for unpublished local extensions in ext-studio
|
|
11
|
+
- Extension install count shown in marketplace
|
|
12
|
+
- Update badge and installed version shown in marketplace list
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
- Extension version history styled with markdown rendering
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Preserve user data on extension update (overwrite instead of replace)
|
|
19
|
+
- Extension install timeout increased to 300s (was too short for large extensions)
|
|
20
|
+
- Installed tab search and loading state in extension store
|
|
21
|
+
- Prevent license loss from non-atomic brand.yml writes
|
|
22
|
+
- Extension detail page content no longer cut off or unscrollable
|
|
23
|
+
- Extension author label position corrected to end of title row
|
|
24
|
+
|
|
25
|
+
## [1.3.10] - 2026-07-09
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Extension author shown inline in title row on extension cards
|
|
29
|
+
- Version input in publish modal — writes back to `ext.yml`, shows friendly Chinese error on version conflict
|
|
30
|
+
- Local extensions now included in the installed extensions list
|
|
31
|
+
|
|
32
|
+
### Improved
|
|
33
|
+
- Published extensions list in publish panel — better style and UX
|
|
34
|
+
- Publishing state, success label, and done button added to publish modal flow
|
|
35
|
+
- OSS CDN prioritized for latest version check (faster upgrade detection)
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Agent card description clamped to 2 lines; author hidden for built-in agents
|
|
39
|
+
- New-session scrollbar centering — use padding calc instead of max-width
|
|
40
|
+
- `file://` links with spaces and non-ASCII paths now render correctly (#350)
|
|
41
|
+
- Invalid API key error message improved to cover expired key case
|
|
42
|
+
- Local extension card in ext-studio now correctly appends action buttons
|
|
43
|
+
- Navigate back to list when extension detail fails to load
|
|
44
|
+
- Page reloads correctly after install/enable/disable/uninstall extension
|
|
45
|
+
- Enriched installed extensions with market data; shows unlisted badge
|
|
46
|
+
|
|
7
47
|
## [1.3.9] - 2026-07-08
|
|
8
48
|
|
|
9
49
|
### Added
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -92,17 +92,25 @@ module Clacky
|
|
|
92
92
|
instance = new(data)
|
|
93
93
|
instance.ensure_device_id!
|
|
94
94
|
instance
|
|
95
|
-
rescue StandardError
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
# A read/parse failure here is almost always a transient corruption (e.g. a
|
|
97
|
+
# concurrent write caught mid-truncate). Do NOT touch brand.yml — overwriting
|
|
98
|
+
# it with an empty config would permanently wipe the license, and even moving
|
|
99
|
+
# it aside could lose a file that self-heals a few milliseconds later. Leave
|
|
100
|
+
# the on-disk file untouched and return an in-memory-only config (device_id
|
|
101
|
+
# generated but never saved); the next load usually succeeds.
|
|
102
|
+
Clacky::Logger.error("[Brand] load failed, using in-memory fallback WITHOUT touching brand.yml: #{e.class}: #{e.message}")
|
|
96
103
|
instance = new({})
|
|
97
|
-
instance.ensure_device_id!
|
|
104
|
+
instance.ensure_device_id!(persist: false)
|
|
98
105
|
instance
|
|
99
106
|
end
|
|
100
107
|
|
|
101
|
-
def ensure_device_id!
|
|
108
|
+
def ensure_device_id!(persist: true)
|
|
102
109
|
return if @device_id && !@device_id.strip.empty?
|
|
103
110
|
|
|
111
|
+
Clacky::Logger.warn("[Brand] regenerating device_id (previous was blank; brand.yml may have been reset or corrupted)")
|
|
104
112
|
@device_id = generate_device_id
|
|
105
|
-
save
|
|
113
|
+
save if persist
|
|
106
114
|
end
|
|
107
115
|
|
|
108
116
|
# Returns true when this installation has a product name configured.
|
|
@@ -159,10 +167,28 @@ module Clacky
|
|
|
159
167
|
end
|
|
160
168
|
|
|
161
169
|
# Save current state to ~/.clacky/brand.yml
|
|
170
|
+
#
|
|
171
|
+
# Writes atomically: content goes to a per-process temp file that is fsynced
|
|
172
|
+
# and then renamed over the target. rename is atomic within a filesystem, so
|
|
173
|
+
# concurrent processes (server + CLI, or multiple servers) never observe a
|
|
174
|
+
# truncated or half-written file — which previously caused load to fall back
|
|
175
|
+
# to an empty config and wipe the license.
|
|
162
176
|
def save
|
|
163
177
|
FileUtils.mkdir_p(CONFIG_DIR)
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
Clacky::Logger.debug("[Brand] save: product_name=#{@product_name.inspect} license_key?=#{!@license_key.nil? && !@license_key.to_s.empty?} device_id?=#{!@device_id.nil? && !@device_id.to_s.empty?}")
|
|
179
|
+
|
|
180
|
+
tmp_path = "#{BRAND_FILE}.#{Process.pid}.#{SecureRandom.hex(4)}.tmp"
|
|
181
|
+
begin
|
|
182
|
+
File.open(tmp_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |f|
|
|
183
|
+
f.write(to_yaml)
|
|
184
|
+
f.flush
|
|
185
|
+
f.fsync
|
|
186
|
+
end
|
|
187
|
+
File.rename(tmp_path, BRAND_FILE)
|
|
188
|
+
rescue StandardError
|
|
189
|
+
File.delete(tmp_path) if File.exist?(tmp_path)
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
166
192
|
end
|
|
167
193
|
|
|
168
194
|
# Remove the local license binding and wipe all brand-related fields from disk.
|
|
@@ -120,6 +120,26 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
120
120
|
json(extensions: exts)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# DELETE /api/ext/ext-studio/local
|
|
124
|
+
# body: { ext_id }
|
|
125
|
+
# Permanently removes a local extension directory. Only allowed for
|
|
126
|
+
# unpublished extensions (caller must check; server enforces dir existence).
|
|
127
|
+
delete "/local" do
|
|
128
|
+
ext_id = require_ext_id!
|
|
129
|
+
|
|
130
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
131
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
132
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
133
|
+
error!("not a local extension", status: 422) unless container[:layer] == :local
|
|
134
|
+
|
|
135
|
+
dir = container[:dir]
|
|
136
|
+
error!("extension directory not found", status: 404) unless Dir.exist?(dir)
|
|
137
|
+
|
|
138
|
+
FileUtils.rm_rf(dir)
|
|
139
|
+
Clacky::ExtensionLoader.load_all(force: true)
|
|
140
|
+
json(ok: true, ext_id: ext_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
123
143
|
# POST /api/ext/ext-studio/unpublish
|
|
124
144
|
# body: { ext_id }
|
|
125
145
|
post "/unpublish" do
|
|
@@ -129,7 +149,33 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
129
149
|
json(ok: true, ext_id: ext_id)
|
|
130
150
|
end
|
|
131
151
|
|
|
132
|
-
# POST /api/ext/ext-studio/
|
|
152
|
+
# POST /api/ext/ext-studio/set_version
|
|
153
|
+
# body: { ext_id, version }
|
|
154
|
+
# Writes the new version string back to the local ext.yml.
|
|
155
|
+
post "/set_version" do
|
|
156
|
+
ext_id = require_ext_id!
|
|
157
|
+
version = presence(json_body["version"])
|
|
158
|
+
error!("version required", status: 422) unless version
|
|
159
|
+
|
|
160
|
+
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
161
|
+
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
162
|
+
error!("extension not found: #{ext_id}", status: 404) unless container
|
|
163
|
+
|
|
164
|
+
yml_path = File.join(container[:dir], "ext.yml")
|
|
165
|
+
error!("ext.yml not found", status: 404) unless File.exist?(yml_path)
|
|
166
|
+
|
|
167
|
+
content = File.read(yml_path)
|
|
168
|
+
if content =~ /^version:/
|
|
169
|
+
content = content.sub(/^version:.*$/, "version: #{version}")
|
|
170
|
+
else
|
|
171
|
+
content = content.rstrip + "\nversion: #{version}\n"
|
|
172
|
+
end
|
|
173
|
+
File.write(yml_path, content)
|
|
174
|
+
|
|
175
|
+
json(ok: true, ext_id: ext_id, version: version)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
|
|
133
179
|
# body: { idea? }
|
|
134
180
|
# Spawns a session bound to the ext-developer agent, optionally seeded with
|
|
135
181
|
# the user's idea as the first task — the "let AI build it for me" entry.
|
|
@@ -19,8 +19,8 @@ contributes:
|
|
|
19
19
|
- id: ext-developer
|
|
20
20
|
title: Extension Developer
|
|
21
21
|
title_zh: 扩展开发
|
|
22
|
-
description: AI expert that helps you build, debug, and publish
|
|
23
|
-
description_zh:
|
|
22
|
+
description: AI expert that helps you build, debug, and publish extensions
|
|
23
|
+
description_zh: 帮你开发、调试、发布扩展的 AI 专家
|
|
24
24
|
order: 5
|
|
25
25
|
prompt: agents/ext-developer/system_prompt.md
|
|
26
26
|
avatar: agents/ext-developer/avatar.png
|
|
@@ -48,9 +48,11 @@
|
|
|
48
48
|
"btn.unpublish": "Unpublish",
|
|
49
49
|
"published.confirm": "Unpublish {{id}} from the marketplace?",
|
|
50
50
|
"err.generic": "Something went wrong: {{msg}}",
|
|
51
|
+
"err.version_conflict": "Version {{ver}} has already been published. Please enter a higher version number above and try again.",
|
|
52
|
+
"pub.version.label": "Version",
|
|
51
53
|
|
|
52
54
|
"publish.confirm.title": "Publish to the marketplace?",
|
|
53
|
-
"publish.confirm.body": "\"{{id}}\" will be packed and uploaded to the
|
|
55
|
+
"publish.confirm.body": "\"{{id}}\" will be packed and uploaded to the extension marketplace, where anyone can discover and install it. Make sure it's ready to share publicly.",
|
|
54
56
|
"publish.confirm.ok": "Publish",
|
|
55
57
|
"publish.confirm.cancel": "Cancel",
|
|
56
58
|
"bind.title": "Authorize this device",
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
|
|
68
70
|
"pub.title.new": "Publish to the marketplace",
|
|
69
71
|
"pub.title.update": "Publish a new version",
|
|
70
|
-
"pub.intro": "\"{{name}}\" will be packed and uploaded to the
|
|
72
|
+
"pub.intro": "\"{{name}}\" will be packed and uploaded to the extension marketplace, where anyone can discover and install it.",
|
|
71
73
|
"pub.version.new": "First release · v{{ver}}",
|
|
72
74
|
"pub.version.update": "v{{prev}} → v{{ver}}",
|
|
73
75
|
"pub.version.missing": "No version in ext.yml — add a \"version\" field before publishing.",
|
|
@@ -77,10 +79,13 @@
|
|
|
77
79
|
"pub.readme.ok": "README.md detected",
|
|
78
80
|
"pub.readme.missing": "No README.md — consider adding one so users understand your extension.",
|
|
79
81
|
"pub.btn.publish": "Publish v{{ver}}",
|
|
82
|
+
"pub.btn.publishing": "Publishing…",
|
|
83
|
+
"pub.btn.done": "Done",
|
|
80
84
|
"pub.btn.cancel": "Cancel",
|
|
81
85
|
"pub.progress.packing": "Packing…",
|
|
82
86
|
"pub.progress.uploading": "Uploading to marketplace…",
|
|
83
87
|
"pub.done": "Published {{id}} v{{ver}} — {{status}}",
|
|
88
|
+
"pub.success": "Published successfully",
|
|
84
89
|
"pub.done.close": "Done",
|
|
85
90
|
"pub.retry": "Try again",
|
|
86
91
|
"extlist.section.cloud": "Published Extensions",
|
|
@@ -95,12 +100,14 @@
|
|
|
95
100
|
"extlist.verify.ok": "Checks passed",
|
|
96
101
|
"extlist.verify.errors": "{{n}} error(s)",
|
|
97
102
|
"extlist.verify.warnings": "{{n}} warning(s)",
|
|
98
|
-
"extlist.btn.publish": "Publish",
|
|
103
|
+
"extlist.btn.publish": "Publish to Marketplace",
|
|
99
104
|
"extlist.btn.update": "Update",
|
|
100
|
-
"extlist.btn.pack": "Pack",
|
|
105
|
+
"extlist.btn.pack": "Pack (.zip)",
|
|
101
106
|
"extlist.btn.packing": "Packing…",
|
|
102
107
|
"extlist.btn.unpublish": "Unpublish",
|
|
103
108
|
"extlist.btn.iterate": "Iterate",
|
|
109
|
+
"extlist.btn.delete": "Delete",
|
|
110
|
+
"extlist.delete.confirm": "Delete local extension \"{{id}}\"? This cannot be undone.",
|
|
104
111
|
"extlist.iterate.seed": "Iterate on extension {{id}}",
|
|
105
112
|
"extlist.changelog.prompt": "Changelog (optional):",
|
|
106
113
|
"extlist.overwrite.confirm": "\"{{id}}\" is already published. Publish a new version?",
|
|
@@ -132,7 +139,7 @@
|
|
|
132
139
|
"skills.shadow.tooltip": "Local copy shadows a same-named brand skill",
|
|
133
140
|
"skills.newSkill.label": "Create a new skill with /skill-creator",
|
|
134
141
|
"skills.newSkill.btn": "Create New Skill",
|
|
135
|
-
"skills.promo.text": "Publish your skills & build your own brand
|
|
142
|
+
"skills.promo.text": "Publish your skills & build your own brand.",
|
|
136
143
|
"skills.promo.link": "Learn more →",
|
|
137
144
|
"skills.locked": "Creator license required to publish cloud skills.",
|
|
138
145
|
"skills.publishing": "Publishing…",
|
|
@@ -178,9 +185,11 @@
|
|
|
178
185
|
"btn.unpublish": "下架",
|
|
179
186
|
"published.confirm": "确定要从市场下架 {{id}} 吗?",
|
|
180
187
|
"err.generic": "出错了:{{msg}}",
|
|
188
|
+
"err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
|
|
189
|
+
"pub.version.label": "版本号",
|
|
181
190
|
|
|
182
191
|
"publish.confirm.title": "确定发布到市场?",
|
|
183
|
-
"publish.confirm.body": "「{{id}}
|
|
192
|
+
"publish.confirm.body": "「{{id}}」将被打包并上传到扩展市场,任何人都能发现并安装它。请确认它已准备好公开分享。",
|
|
184
193
|
"publish.confirm.ok": "发布",
|
|
185
194
|
"publish.confirm.cancel": "取消",
|
|
186
195
|
"bind.title": "授权此设备",
|
|
@@ -197,7 +206,7 @@
|
|
|
197
206
|
|
|
198
207
|
"pub.title.new": "发布到市场",
|
|
199
208
|
"pub.title.update": "发布新版本",
|
|
200
|
-
"pub.intro": "「{{name}}
|
|
209
|
+
"pub.intro": "「{{name}}」将被打包并上传到扩展市场,任何人都能发现并安装它。",
|
|
201
210
|
"pub.version.new": "首次发布 · v{{ver}}",
|
|
202
211
|
"pub.version.update": "v{{prev}} → v{{ver}}",
|
|
203
212
|
"pub.version.missing": "ext.yml 里没有 version — 请先补上 version 字段再发布。",
|
|
@@ -207,10 +216,13 @@
|
|
|
207
216
|
"pub.readme.ok": "已检测到 README.md",
|
|
208
217
|
"pub.readme.missing": "未找到 README.md — 建议补一份,方便用户了解你的扩展。",
|
|
209
218
|
"pub.btn.publish": "发布 v{{ver}}",
|
|
219
|
+
"pub.btn.publishing": "发布中…",
|
|
220
|
+
"pub.btn.done": "完成",
|
|
210
221
|
"pub.btn.cancel": "取消",
|
|
211
222
|
"pub.progress.packing": "打包中…",
|
|
212
223
|
"pub.progress.uploading": "上传到市场…",
|
|
213
224
|
"pub.done": "已发布 {{id}} v{{ver}} — {{status}}",
|
|
225
|
+
"pub.success": "已发布成功",
|
|
214
226
|
"pub.done.close": "完成",
|
|
215
227
|
"pub.retry": "重试",
|
|
216
228
|
"extlist.section.cloud": "已发布扩展",
|
|
@@ -225,12 +237,14 @@
|
|
|
225
237
|
"extlist.verify.ok": "检查通过",
|
|
226
238
|
"extlist.verify.errors": "{{n}} 个错误",
|
|
227
239
|
"extlist.verify.warnings": "{{n}} 个警告",
|
|
228
|
-
"extlist.btn.publish": "
|
|
229
|
-
"extlist.btn.update": "
|
|
230
|
-
"extlist.btn.pack": "打包",
|
|
240
|
+
"extlist.btn.publish": "发布到市场",
|
|
241
|
+
"extlist.btn.update": "更新到市场",
|
|
242
|
+
"extlist.btn.pack": "打包(.zip)",
|
|
231
243
|
"extlist.btn.packing": "打包中…",
|
|
232
244
|
"extlist.btn.unpublish": "下架",
|
|
233
245
|
"extlist.btn.iterate": "迭代",
|
|
246
|
+
"extlist.btn.delete": "删除",
|
|
247
|
+
"extlist.delete.confirm": "确定要删除本地扩展「{{id}}」吗?此操作不可恢复。",
|
|
234
248
|
"extlist.iterate.seed": "迭代扩展 {{id}}",
|
|
235
249
|
"extlist.changelog.prompt": "更新说明(可选):",
|
|
236
250
|
"extlist.overwrite.confirm": "「{{id}}」已经发布过了。要发布新版本吗?",
|
|
@@ -262,7 +276,7 @@
|
|
|
262
276
|
"skills.shadow.tooltip": "本地副本覆盖了同名品牌 skill",
|
|
263
277
|
"skills.newSkill.label": "用 /skill-creator 创建新 skill",
|
|
264
278
|
"skills.newSkill.btn": "创建新 Skill",
|
|
265
|
-
"skills.promo.text": "发布你的 skill
|
|
279
|
+
"skills.promo.text": "发布你的 skill,打造自己的品牌。",
|
|
266
280
|
"skills.promo.link": "了解更多 →",
|
|
267
281
|
"skills.locked": "发布云端 skill 需要创作者授权。",
|
|
268
282
|
"skills.publishing": "发布中…",
|
|
@@ -465,44 +479,48 @@
|
|
|
465
479
|
|
|
466
480
|
// Unified publish flow: one modal that walks the creator from a release form
|
|
467
481
|
// (version + notes) through progress → device binding (if needed) → done.
|
|
468
|
-
// The creator owns the version
|
|
469
|
-
//
|
|
470
|
-
//
|
|
482
|
+
// The creator owns the version; if it conflicts with the latest published
|
|
483
|
+
// version they can bump it inline — the modal writes it back to ext.yml
|
|
484
|
+
// before uploading. `prevVersionOrPromise` may be a value or a Promise.
|
|
471
485
|
// Resolves true when a publish succeeded, false otherwise.
|
|
472
|
-
function runPublishFlow(ext,
|
|
486
|
+
function runPublishFlow(ext, prevVersionOrPromise) {
|
|
473
487
|
return new Promise((resolve) => {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
metaBlock.appendChild(el("span", { class: "studio-pub-version", text: verText }));
|
|
488
|
+
let currentVersion = ext.version || "";
|
|
489
|
+
let isUpdate = false;
|
|
490
|
+
|
|
491
|
+
const verField = el("div", { class: "studio-field" });
|
|
492
|
+
verField.appendChild(el("label", { class: "studio-label", text: t("pub.version.label") }));
|
|
493
|
+
const verInput = el("input", { class: "studio-input", type: "text", value: currentVersion, placeholder: "1.0.0" });
|
|
494
|
+
verInput.addEventListener("input", () => {
|
|
495
|
+
currentVersion = verInput.value.trim();
|
|
496
|
+
publishBtn.disabled = !currentVersion;
|
|
497
|
+
publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
|
|
498
|
+
});
|
|
499
|
+
verField.appendChild(verInput);
|
|
487
500
|
|
|
488
501
|
const notesField = el("div", { class: "studio-field" });
|
|
489
502
|
notesField.appendChild(el("label", { class: "studio-label", text: t("pub.notes.label") }));
|
|
490
|
-
const notes = el("textarea", { class: "studio-textarea", rows: "
|
|
503
|
+
const notes = el("textarea", { class: "studio-textarea", rows: "4", placeholder: t("pub.notes.placeholder") });
|
|
491
504
|
notesField.appendChild(notes);
|
|
492
505
|
|
|
493
506
|
const status = el("p", { class: "studio-modal-status" });
|
|
494
507
|
status.style.display = "none";
|
|
495
508
|
|
|
496
|
-
const bodyChildren = [
|
|
509
|
+
const bodyChildren = [
|
|
510
|
+
el("p", { class: "studio-modal-intro", text: t("pub.intro", { name: ext.name }) }),
|
|
511
|
+
verField,
|
|
512
|
+
notesField,
|
|
513
|
+
status,
|
|
514
|
+
];
|
|
497
515
|
|
|
498
516
|
let done = false;
|
|
499
517
|
const modal = openModal({
|
|
500
|
-
title:
|
|
518
|
+
title: t("pub.title.new"),
|
|
501
519
|
body: el("div", null, bodyChildren),
|
|
502
520
|
buttons: [
|
|
503
521
|
{ label: t("pub.btn.cancel"), keepOpen: false, onClick: () => { if (!done) resolve(false); } },
|
|
504
522
|
{
|
|
505
|
-
label:
|
|
523
|
+
label: currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" }),
|
|
506
524
|
primary: true,
|
|
507
525
|
keepOpen: true,
|
|
508
526
|
onClick: () => submit(),
|
|
@@ -512,50 +530,82 @@
|
|
|
512
530
|
|
|
513
531
|
const publishBtn = modal.footer.querySelector(".studio-btn-primary");
|
|
514
532
|
const cancelBtn = modal.footer.querySelector(".studio-btn:not(.studio-btn-primary)");
|
|
515
|
-
if (!
|
|
533
|
+
if (!currentVersion) publishBtn.disabled = true;
|
|
534
|
+
|
|
535
|
+
Promise.resolve(prevVersionOrPromise).then((prevVersion) => {
|
|
536
|
+
if (done || !prevVersion) return;
|
|
537
|
+
isUpdate = true;
|
|
538
|
+
const titleEl = modal.overlay.querySelector(".studio-modal-title");
|
|
539
|
+
if (titleEl) titleEl.textContent = t("pub.title.update");
|
|
540
|
+
if (prevVersion !== currentVersion) {
|
|
541
|
+
currentVersion = prevVersion;
|
|
542
|
+
verInput.value = currentVersion;
|
|
543
|
+
publishBtn.disabled = false;
|
|
544
|
+
publishBtn.textContent = t("pub.btn.publish", { ver: currentVersion });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
516
547
|
|
|
517
|
-
function setProgress(msg) {
|
|
548
|
+
function setProgress(msg, isError) {
|
|
518
549
|
status.style.display = "";
|
|
519
550
|
status.textContent = msg;
|
|
551
|
+
status.className = "studio-modal-status" + (isError ? " studio-modal-status-error" : "");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function resetButtons() {
|
|
555
|
+
publishBtn.disabled = !currentVersion;
|
|
556
|
+
publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
|
|
557
|
+
cancelBtn.disabled = false;
|
|
558
|
+
notes.disabled = false;
|
|
559
|
+
verInput.disabled = false;
|
|
520
560
|
}
|
|
521
561
|
|
|
522
562
|
async function submit() {
|
|
523
563
|
if (done || publishBtn.disabled) return;
|
|
564
|
+
const ver = currentVersion;
|
|
565
|
+
if (!ver) return;
|
|
566
|
+
|
|
524
567
|
publishBtn.disabled = true;
|
|
568
|
+
publishBtn.textContent = t("pub.btn.publishing");
|
|
525
569
|
cancelBtn.disabled = true;
|
|
526
570
|
notes.disabled = true;
|
|
527
|
-
|
|
571
|
+
verInput.disabled = true;
|
|
572
|
+
status.style.display = "none";
|
|
573
|
+
|
|
528
574
|
try {
|
|
529
|
-
|
|
575
|
+
if (ver !== ext.version) {
|
|
576
|
+
await postJson("/set_version", { ext_id: ext.id, version: ver });
|
|
577
|
+
}
|
|
578
|
+
|
|
530
579
|
const data = await publishWithBinding({
|
|
531
580
|
ext_id: ext.id,
|
|
532
581
|
force: isUpdate,
|
|
533
582
|
changelog: notes.value.trim(),
|
|
534
583
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
cancelBtn.disabled = false;
|
|
538
|
-
notes.disabled = false;
|
|
539
|
-
status.style.display = "none";
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
584
|
+
|
|
585
|
+
if (data === null) { resetButtons(); return; }
|
|
542
586
|
if (!data.ok) throw new Error(data.error || "Publish failed");
|
|
587
|
+
|
|
543
588
|
done = true;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
status: data.status || "",
|
|
548
|
-
}));
|
|
549
|
-
publishBtn.style.display = "none";
|
|
550
|
-
cancelBtn.textContent = t("pub.done.close");
|
|
551
|
-
cancelBtn.disabled = false;
|
|
552
|
-
cancelBtn.onclick = () => { modal.close(); resolve(true); };
|
|
553
|
-
} catch (e) {
|
|
554
|
-
setProgress(t("err.generic", { msg: e.message }));
|
|
589
|
+
const successLabel = el("span", { class: "studio-pub-success", text: t("pub.success") });
|
|
590
|
+
modal.footer.insertBefore(successLabel, modal.footer.firstChild);
|
|
591
|
+
cancelBtn.style.display = "none";
|
|
555
592
|
publishBtn.disabled = false;
|
|
556
|
-
publishBtn.textContent = t("pub.
|
|
557
|
-
|
|
558
|
-
|
|
593
|
+
publishBtn.textContent = t("pub.btn.done");
|
|
594
|
+
publishBtn.onclick = () => { modal.close(); resolve(true); };
|
|
595
|
+
} catch (e) {
|
|
596
|
+
const msg = e.message || "";
|
|
597
|
+
const isConflict = /must be greater than/i.test(msg);
|
|
598
|
+
if (isConflict) {
|
|
599
|
+
setProgress(t("err.version_conflict", { ver }), true);
|
|
600
|
+
verInput.classList.add("studio-input-error");
|
|
601
|
+
verInput.disabled = false;
|
|
602
|
+
verInput.focus();
|
|
603
|
+
verInput.select();
|
|
604
|
+
verInput.addEventListener("input", () => verInput.classList.remove("studio-input-error"), { once: true });
|
|
605
|
+
} else {
|
|
606
|
+
setProgress(t("err.generic", { msg }), true);
|
|
607
|
+
}
|
|
608
|
+
resetButtons();
|
|
559
609
|
}
|
|
560
610
|
}
|
|
561
611
|
});
|
|
@@ -739,13 +789,13 @@
|
|
|
739
789
|
async function doPublish() {
|
|
740
790
|
const ext = store.selected();
|
|
741
791
|
if (!ext) return;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const ok = await runPublishFlow(ext,
|
|
792
|
+
const prevVersionPromise = getJson("/published")
|
|
793
|
+
.then((data) => {
|
|
794
|
+
const match = (data.extensions || []).find((e) => e.id === ext.id);
|
|
795
|
+
return match ? match.version : null;
|
|
796
|
+
})
|
|
797
|
+
.catch(() => null);
|
|
798
|
+
const ok = await runPublishFlow(ext, prevVersionPromise);
|
|
749
799
|
if (ok) loadPublished();
|
|
750
800
|
}
|
|
751
801
|
|
|
@@ -774,17 +824,23 @@
|
|
|
774
824
|
const exts = data.extensions || [];
|
|
775
825
|
if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
|
|
776
826
|
exts.forEach((e) => {
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
827
|
+
const card = el("div", { class: "studio-skill-card" });
|
|
828
|
+
const head = el("div", { class: "studio-skill-head" });
|
|
829
|
+
head.appendChild(el("span", { class: "studio-skill-name", text: e.name || e.id }));
|
|
830
|
+
const statusKind = e.status === "draft" ? "local" : "published";
|
|
831
|
+
head.appendChild(el("span", { class: `studio-skill-badge studio-skill-badge-${statusKind}`, text: e.status === "draft" ? t("extlist.badge.draft") : t("extlist.badge.published") }));
|
|
832
|
+
if (e.version) head.appendChild(el("span", { style: "font-size:11px;color:var(--color-text-muted);", text: "v" + e.version }));
|
|
833
|
+
const unpubBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("btn.unpublish") });
|
|
834
|
+
unpubBtn.style.marginLeft = "auto";
|
|
835
|
+
unpubBtn.addEventListener("click", async () => {
|
|
836
|
+
if (!window.confirm(t("published.confirm", { id: e.id }))) return;
|
|
837
|
+
unpubBtn.disabled = true;
|
|
838
|
+
try { await postJson("/unpublish", { ext_id: e.id }); loadPublished(); }
|
|
839
|
+
catch (err) { unpubBtn.disabled = false; feedback(t("err.generic", { msg: err.message }), "error"); }
|
|
840
|
+
});
|
|
841
|
+
head.appendChild(unpubBtn);
|
|
842
|
+
card.appendChild(head);
|
|
843
|
+
box.appendChild(card);
|
|
788
844
|
});
|
|
789
845
|
} catch (e) {
|
|
790
846
|
box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
|
|
@@ -927,6 +983,13 @@
|
|
|
927
983
|
const packBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack") });
|
|
928
984
|
packBtn.addEventListener("click", () => doPack(ext, packBtn));
|
|
929
985
|
actions.appendChild(packBtn);
|
|
986
|
+
card.appendChild(actions);
|
|
987
|
+
|
|
988
|
+
if (!published) {
|
|
989
|
+
const delBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("extlist.btn.delete") });
|
|
990
|
+
delBtn.addEventListener("click", () => doDelete(ext, delBtn));
|
|
991
|
+
actions.appendChild(delBtn);
|
|
992
|
+
}
|
|
930
993
|
return card;
|
|
931
994
|
}
|
|
932
995
|
|
|
@@ -942,6 +1005,25 @@
|
|
|
942
1005
|
}
|
|
943
1006
|
}
|
|
944
1007
|
|
|
1008
|
+
async function doDelete(ext, btn) {
|
|
1009
|
+
if (!confirm(t("extlist.delete.confirm", { id: ext.id }))) return;
|
|
1010
|
+
btn.disabled = true;
|
|
1011
|
+
try {
|
|
1012
|
+
const res = await fetch("/api/ext/ext-studio/local", {
|
|
1013
|
+
method: "DELETE",
|
|
1014
|
+
headers: { "Content-Type": "application/json" },
|
|
1015
|
+
body: JSON.stringify({ ext_id: ext.id }),
|
|
1016
|
+
});
|
|
1017
|
+
let data = {};
|
|
1018
|
+
try { data = await res.json(); } catch (_e) {}
|
|
1019
|
+
if (!res.ok) throw new Error(data?.error || `Error ${res.status}`);
|
|
1020
|
+
await reload(); rebuild();
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
alert(t("err.generic", { msg: e.message }));
|
|
1023
|
+
btn.disabled = false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
945
1027
|
async function doPublish(ext, prevVersion) {
|
|
946
1028
|
const ok = await runPublishFlow(ext, prevVersion);
|
|
947
1029
|
if (ok) { await reload(); rebuild(); }
|
|
@@ -1269,7 +1351,7 @@
|
|
|
1269
1351
|
.studio-btn-primary { background: var(--color-button-primary); color: var(--color-button-primary-text); border-color: transparent; }
|
|
1270
1352
|
.studio-btn-primary:hover { background: var(--color-button-primary-hover); color: var(--color-button-primary-text); }
|
|
1271
1353
|
.studio-btn-primary:disabled { opacity: 0.6; cursor: default; }
|
|
1272
|
-
.studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border);
|
|
1354
|
+
.studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); }
|
|
1273
1355
|
.studio-btn-danger:hover { background: var(--color-error-bg); color: var(--color-error); }
|
|
1274
1356
|
.studio-btn-ghost { color: var(--color-text-secondary); font-weight: 500; }
|
|
1275
1357
|
.studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
|
|
@@ -1291,8 +1373,8 @@
|
|
|
1291
1373
|
.studio-feedback-error { color: var(--color-error); }
|
|
1292
1374
|
.studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
|
|
1293
1375
|
.studio-published { margin-top: 16px; border-top: 1px solid var(--color-border-primary); padding-top: 12px; }
|
|
1294
|
-
.studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding:
|
|
1295
|
-
.studio-published-name { color: var(--color-text-primary); }
|
|
1376
|
+
.studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
|
|
1377
|
+
.studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
|
|
1296
1378
|
.studio-page { width: 100%; }
|
|
1297
1379
|
.studio-page-head { margin-bottom: 20px; }
|
|
1298
1380
|
.studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
|
|
@@ -1305,7 +1387,7 @@
|
|
|
1305
1387
|
.studio-skill-section { margin-bottom: 28px; }
|
|
1306
1388
|
.studio-skill-section-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
|
|
1307
1389
|
.studio-skill-hint { font-size: 11px; color: var(--color-text-muted); }
|
|
1308
|
-
.studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding:
|
|
1390
|
+
.studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 12px; margin: 0 0 10px; background: var(--color-bg-secondary); }
|
|
1309
1391
|
.studio-skill-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
1310
1392
|
.studio-skill-name { font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
|
|
1311
1393
|
.studio-skill-badge { font-size: 11px; padding: 1px 8px; border-radius: 10px; }
|
|
@@ -1314,7 +1396,7 @@
|
|
|
1314
1396
|
.studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
|
|
1315
1397
|
.studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
|
|
1316
1398
|
.studio-skill-desc { margin: 6px 0 8px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
|
|
1317
|
-
.studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted);
|
|
1399
|
+
.studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); }
|
|
1318
1400
|
.studio-skill-promo { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 16px 18px; margin: 0 0 24px; background: var(--color-bg-secondary); }
|
|
1319
1401
|
.studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
|
|
1320
1402
|
.studio-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; animation: studio-overlay-in 0.15s ease; }
|
|
@@ -1329,9 +1411,14 @@
|
|
|
1329
1411
|
.studio-pub-units::before { content: "·"; margin-right: 10px; }
|
|
1330
1412
|
.studio-modal-body { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
|
|
1331
1413
|
.studio-modal-status { margin: 0 0 8px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
|
|
1414
|
+
.studio-modal-status-error { color: var(--color-error); }
|
|
1415
|
+
.studio-input { width: 100%; box-sizing: border-box; background: var(--color-bg-input); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 7px 10px; color: var(--color-text-primary); font-size: 13px; font-family: monospace; }
|
|
1416
|
+
.studio-input:focus { border-color: var(--color-accent-primary); outline: none; }
|
|
1417
|
+
.studio-input-error { border-color: var(--color-error) !important; }
|
|
1332
1418
|
.studio-modal-code { margin: 0 0 8px; font-size: 13px; font-family: monospace; color: var(--color-text-primary); }
|
|
1333
1419
|
.studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
|
|
1334
1420
|
.studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--color-border-primary); }
|
|
1421
|
+
.studio-pub-success { margin-right: auto; font-size: 13px; font-weight: 500; color: var(--color-success); }
|
|
1335
1422
|
`;
|
|
1336
1423
|
document.head.appendChild(style);
|
|
1337
1424
|
})();
|