@follenfang/fupload 0.0.1 → 0.0.2
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 +39 -11
- package/fupload/SKILL.md +22 -9
- package/fupload/agents/openai.yaml +2 -2
- package/fupload/examples/curseforge-plugin-upload.json +21 -0
- package/fupload/references/curseforge.md +233 -0
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/cli.py +18 -3
- package/fupload/scripts/fupload_cli/curseforge.py +186 -0
- package/fupload/scripts/fupload_cli/schema.py +48 -0
- package/npm/bin/fupload.mjs +2 -0
- package/npm/lib/curseforge-config.mjs +36 -0
- package/npm/lib/update.mjs +3 -0
- package/npm/postinstall.mjs +3 -0
- package/npm/skill-manifest.json +27 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Fuploader
|
|
2
2
|
|
|
3
|
-
Fuploader 是一个面向 Agent 的《魔兽世界》作者发布 Skill 和 CLI。npm 安装后统一使用 `fupload` 命令;命令内部运行随 Skill 分发的纯 Python 实现。新手盒子(NewBeeBox)默认优先使用官方 `ncc` CLI,用户显式要求第三方管理工具时才使用 Fuploader 通道;网易 DD 使用 Fuploader
|
|
3
|
+
Fuploader 是一个面向 Agent 的《魔兽世界》作者发布 Skill 和 CLI。npm 安装后统一使用 `fupload` 命令;命令内部运行随 Skill 分发的纯 Python 实现。新手盒子(NewBeeBox)默认优先使用官方 `ncc` CLI,用户显式要求第三方管理工具时才使用 Fuploader 通道;网易 DD 使用 Fuploader 调用官方无头客户端;CurseForge 使用 Core API 查询作者公开项目,并使用 Authors Upload API 上传插件 ZIP。
|
|
4
4
|
|
|
5
5
|
项目强调显式调用、完整字段收集、写入前确认和写入后读回验证。CLI 只负责单次原子读写,业务选择、执行计划和异常恢复由 Agent 在对话中完成。
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- 支持新手盒子官方 `ncc`、第三方 Python
|
|
9
|
+
- 支持新手盒子官方 `ncc`、第三方 Python 管理通道、网易 DD 和 CurseForge。
|
|
10
|
+
- CurseForge 支持按作者 ID 查询 WoW 公开项目、读取上传用游戏版本,以及向已有项目上传插件 ZIP;不创建 Project,也不枚举私有、草稿或待审 Project。
|
|
10
11
|
- 支持插件、配置分享、WA/字符串的创建、内容更新与元数据编辑。
|
|
11
12
|
- 官方通道严格采用已安装 `ncc` 暴露的能力;第三方 Python 通道覆盖版本、游戏分支、分类、媒体、可见性、审核、商业设置、频道、关联内容和配置备份选择等页面字段。
|
|
12
13
|
- 第三方 Python 以 Creator Center 网页的请求和表单状态为基准,编辑和更新采用 `GET -> 动态选项查询 -> presence-aware patch -> 写入 -> 读回`;关联作者/内容在主记录读回后替换并再次读回。官方通道采用 `ncc` 文档规定的只读查询、写入和读回命令。
|
|
@@ -54,6 +55,16 @@ fupload --help
|
|
|
54
55
|
|
|
55
56
|
全局安装会同时创建 `fupload` 命令,并把版本匹配的 Skill 原子安装到 `~/.agents/skills/fupload/`。可用 `FUPLOAD_AGENT_HOME` 把默认位置改为 `<home>/skills/fupload`,或在单次命令中使用 `--skill-dir <完整路径>`;所有成功管理过的 Skill 路径都会登记,供完整卸载使用。
|
|
56
57
|
|
|
58
|
+
安装和 `fupload update` 还会幂等创建缺失的 `~/.fupload/curseforge.env`;已有文件逐字节保留,不覆盖或补写其中的值:
|
|
59
|
+
|
|
60
|
+
```dotenv
|
|
61
|
+
CURSEFORGE_AUTHOR_ID=
|
|
62
|
+
CURSEFORGE_API_KEY=
|
|
63
|
+
CURSEFORGE_UPLOAD_TOKEN=
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`CURSEFORGE_AUTHOR_ID` 是可在对话中提供的非秘密数字 ID;Core API Key 与 Upload Token 是两套不同秘密。请只在本机 env 文件或进程环境中填写秘密,不要粘贴到 Agent 对话、命令参数、项目文件或 Git。
|
|
67
|
+
|
|
57
68
|
## 作为 Skill 使用
|
|
58
69
|
|
|
59
70
|
安装后显式调用:
|
|
@@ -62,7 +73,7 @@ fupload --help
|
|
|
62
73
|
$fupload
|
|
63
74
|
```
|
|
64
75
|
|
|
65
|
-
该 Skill
|
|
76
|
+
该 Skill 不会因普通提及“发布”“新手盒子”“DD”或“CurseForge”而自动触发。Agent 会先询问平台、资源和动作。新手盒子会自动检测 `ncc`:已安装即默认使用官方通道;未安装时先询问是否安装;只有用户显式选择第三方 Python 管理工具才进入项目内 CLI。CurseForge 上传缺少作者 ID 时,Agent 会主动询问;缺少 API Key 或 Upload Token 时,只会引导用户在本机填写 `~/.fupload/curseforge.env`,不会要求在对话中提供秘密。Agent 在被发布项目中创建 `publish/<时间>-<平台>-<资源>-<动作>/`,同一次发布的脱敏 JSON 按原子步骤保存为 `01-<动作>.json`、`02-<动作>.json`。展示完整写入计划并得到确认后才会真实写入。
|
|
66
77
|
|
|
67
78
|
## CLI 使用
|
|
68
79
|
|
|
@@ -77,7 +88,7 @@ ncc whoami -o json
|
|
|
77
88
|
|
|
78
89
|
创建令牌和本机登录说明见[官方 CLI 文档](https://creator.newbeebox.com/cli-docs)。不要把令牌粘贴到 Agent 对话;在自己的终端完成 `ncc login`。
|
|
79
90
|
|
|
80
|
-
以下为 Fuploader 第三方 NewBeeBox 与
|
|
91
|
+
以下为 Fuploader 第三方 NewBeeBox、DD 与 CurseForge 执行层。
|
|
81
92
|
|
|
82
93
|
查看总帮助:
|
|
83
94
|
|
|
@@ -85,6 +96,22 @@ ncc whoami -o json
|
|
|
85
96
|
fupload --help
|
|
86
97
|
```
|
|
87
98
|
|
|
99
|
+
检查 CurseForge 配置、查询作者公开项目和读取游戏版本:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
fupload curseforge session doctor
|
|
103
|
+
fupload curseforge project list
|
|
104
|
+
fupload curseforge project list --author-id 138844367
|
|
105
|
+
fupload curseforge plugin game-versions
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
上传前先执行本地 dry-run;确认完整计划后再移除 `--dry-run`:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
fupload curseforge plugin upload --input fupload\examples\curseforge-plugin-upload.json --dry-run
|
|
112
|
+
fupload curseforge plugin upload --input publish\20260807-120000-curseforge-plugin-upload\01-upload.json
|
|
113
|
+
```
|
|
114
|
+
|
|
88
115
|
查看具体操作的可执行字段契约:
|
|
89
116
|
|
|
90
117
|
```powershell
|
|
@@ -134,7 +161,7 @@ fupload uninstall
|
|
|
134
161
|
|
|
135
162
|
`fupload update` 固定安装 `@follenfang/fupload@latest`,随后把默认位置和所有登记有效的自定义 Skill 同步到同一版本。未知目录不会被覆盖。
|
|
136
163
|
|
|
137
|
-
`fupload uninstall` 先删除所有仍有有效 npm 管理标记的 Fuploader Skill,再调用 npm 删除 `@follenfang/fupload` 和 `fupload` 命令。Skill 清理失败时 npm 包保持可用,处理占用或权限问题后可重试。项目中的 `publish/`、新手盒子/DD
|
|
164
|
+
`fupload uninstall` 先删除所有仍有有效 npm 管理标记的 Fuploader Skill,再调用 npm 删除 `@follenfang/fupload` 和 `fupload` 命令。Skill 清理失败时 npm 包保持可用,处理占用或权限问题后可重试。项目中的 `publish/`、新手盒子/DD 登录数据、DD 日志和 `~/.fupload/curseforge.env` 始终保留。
|
|
138
165
|
|
|
139
166
|
npm 7 及以上不执行卸载 lifecycle,因此直接运行 `npm uninstall -g @follenfang/fupload` 只删除 npm 包和 CLI,可能留下 Skill;它不属于完整卸载流程。
|
|
140
167
|
|
|
@@ -154,6 +181,7 @@ npm 7 及以上不执行卸载 lifecycle,因此直接运行 `npm uninstall -g
|
|
|
154
181
|
- [新手盒子官方 CLI 完整参考](fupload/references/newbee-official-cli.md)
|
|
155
182
|
- [新手盒子第三方 Python 字段参考](fupload/references/newbee.md)
|
|
156
183
|
- [网易 DD 字段参考](fupload/references/dd.md)
|
|
184
|
+
- [CurseForge API、字段与上传参考](fupload/references/curseforge.md)
|
|
157
185
|
|
|
158
186
|
## 测试
|
|
159
187
|
|
|
@@ -166,7 +194,7 @@ python -m unittest discover -s fupload\scripts\tests
|
|
|
166
194
|
python -m compileall -q fupload\scripts
|
|
167
195
|
```
|
|
168
196
|
|
|
169
|
-
当前回归测试覆盖 CLI 路由、严格 JSON、字段 Schema
|
|
197
|
+
当前回归测试覆盖 CLI 路由、严格 JSON、字段 Schema、平台 builder、动态选项校验、字段保留与清空语义,以及所有内置示例的 dry-run。
|
|
170
198
|
|
|
171
199
|
DD 逐字段 wire 矩阵可单独运行并生成本地审计报告:
|
|
172
200
|
|
|
@@ -184,7 +212,7 @@ python fupload\scripts\tests\generate_dd_wire_matrix_report.py > analyze\dd-fiel
|
|
|
184
212
|
- `fupload/scripts/fupload_cli/__init__.py` 的 `__version__`
|
|
185
213
|
- `npm/skill-manifest.json` 的包版本与 Skill 版本
|
|
186
214
|
|
|
187
|
-
|
|
215
|
+
当前版本为 `0.0.2`,对应 tag `v0.0.2`。`.github/workflows/publish-npm.yml` 在 pull request、`main` push 和 `v*` tag 上运行 Windows/Linux CI;只有 tag CI 全部通过时才通过 npm Trusted Publishing/OIDC 发布,不使用长期 `NPM_TOKEN`。
|
|
188
216
|
|
|
189
217
|
发布前在 npm 包设置中绑定 Trusted Publisher:
|
|
190
218
|
|
|
@@ -197,12 +225,12 @@ python fupload\scripts\tests\generate_dd_wire_matrix_report.py > analyze\dd-fiel
|
|
|
197
225
|
|
|
198
226
|
```powershell
|
|
199
227
|
npm run check:versions
|
|
200
|
-
npm run check:release -- v0.0.
|
|
201
|
-
git tag -a v0.0.
|
|
202
|
-
git push origin v0.0.
|
|
228
|
+
npm run check:release -- v0.0.2
|
|
229
|
+
git tag -a v0.0.2 -m "Fuploader 0.0.2"
|
|
230
|
+
git push origin v0.0.2
|
|
203
231
|
```
|
|
204
232
|
|
|
205
|
-
npm 对首次创建的包可能要求先由账号所有者完成一次 bootstrap 发布;该步骤只用于建立包身份,正式 `latest`
|
|
233
|
+
npm 对首次创建的包可能要求先由账号所有者完成一次 bootstrap 发布;该步骤只用于建立包身份,正式 `latest` 由版本 tag workflow 发布。需要人工处理时,不要把 npm token 写入仓库或 GitHub Secret。
|
|
206
234
|
|
|
207
235
|
## 说明
|
|
208
236
|
|
package/fupload/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fupload
|
|
3
|
-
description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox and
|
|
3
|
+
description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, and CurseForge, including CurseForge author project lookup and plugin ZIP upload. Use only when the user explicitly invokes `$fupload`, explicitly asks to use the Fupload Skill, or loads this Skill by path. Do not trigger from ordinary mentions of publishing, NewBeeBox, DD, CurseForge, plugins, configurations, or WA.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.0.
|
|
5
|
+
version: "0.0.2"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Fupload
|
|
9
9
|
|
|
10
|
-
Act as the publishing operator. For NewBeeBox, prefer the official `ncc` CLI whenever it is installed unless the user explicitly requests the third-party Python management tool. For DD, and
|
|
10
|
+
Act as the publishing operator. For NewBeeBox, prefer the official `ncc` CLI whenever it is installed unless the user explicitly requests the third-party Python management tool. For DD, CurseForge, and an explicitly selected third-party NewBeeBox workflow, use the bundled Python CLI. Keep investigation, choices, planning, confirmation, and recovery in the conversation.
|
|
11
11
|
|
|
12
12
|
When installed from npm, interpret `<fupload-cli>` as `fupload`. Only while maintaining the source repository, directly installing this Skill, or when `fupload` is absent from PATH, use `python <skill-root>/scripts/fupload.py`. Keep the user's current project as the working directory in either mode.
|
|
13
13
|
|
|
@@ -15,8 +15,8 @@ When installed from npm, interpret `<fupload-cli>` as `fupload`. Only while main
|
|
|
15
15
|
|
|
16
16
|
When the user has not already stated both dimensions, ask in one short message:
|
|
17
17
|
|
|
18
|
-
1. Platform: NewBeeBox or
|
|
19
|
-
2. Resource and action: plugin/configuration/WA, then create/content update/metadata edit/delete.
|
|
18
|
+
1. Platform: NewBeeBox, DD, or CurseForge.
|
|
19
|
+
2. Resource and action: plugin/configuration/WA, then create/content update/metadata edit/delete. CurseForge supports public project listing and uploading a plugin ZIP to an existing project only.
|
|
20
20
|
|
|
21
21
|
Map natural language consistently:
|
|
22
22
|
|
|
@@ -27,10 +27,14 @@ Map natural language consistently:
|
|
|
27
27
|
|
|
28
28
|
Never offer bulk delete, version-file delete, drafts, guides, messages, or GUI control automation. The DD task-session command may close verified official GUI processes only after the consent flow below.
|
|
29
29
|
|
|
30
|
+
CurseForge cannot create a project or enumerate private, draft, or pending-review projects. Direct project creation to the Authors website; use the Core API list only as the public-project view for one numeric author ID.
|
|
31
|
+
|
|
30
32
|
## Select the execution channel
|
|
31
33
|
|
|
32
34
|
For DD, use the bundled Python CLI.
|
|
33
35
|
|
|
36
|
+
For CurseForge, use the bundled Python CLI. Do not substitute direct HTTP calls or the CurseForge for Studios API for uploads: public project lookup uses the Core API, while game-version lookup and upload use the separate Upload API and credential.
|
|
37
|
+
|
|
34
38
|
For NewBeeBox, apply this order exactly:
|
|
35
39
|
|
|
36
40
|
1. If the user explicitly requests the third-party Python management tool, use the bundled Python CLI even when `ncc` is installed.
|
|
@@ -44,9 +48,11 @@ Never switch channels silently. Before changing channels, re-read remote state a
|
|
|
44
48
|
|
|
45
49
|
## Protect credentials
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
For NewBeeBox, prefer an existing official `ncc login`, then a caller-provided `NCC_TOKEN`, then user-performed local login. Never ask the user to paste a token into the conversation. Never place a real NewBeeBox token in `--token`, shell history, command output, JSON, `.env`, `publish/`, `analyze/`, tests, Skill files, references, or Git. Do not inspect or copy the official CLI credential store.
|
|
52
|
+
|
|
53
|
+
For CurseForge, use `~/.fupload/curseforge.env`, created idempotently by npm install/update, with `CURSEFORGE_AUTHOR_ID`, `CURSEFORGE_API_KEY`, and `CURSEFORGE_UPLOAD_TOKEN`. The author ID is non-secret: if absent, proactively ask for its numeric value or offer `--author-id`. The API key and upload token are secrets: never ask the user to paste either into the conversation; direct them to fill the local env file in their own editor or terminal, then run `curseforge session doctor`. Never print file contents or secret values. Process environment values may override the file without being shown.
|
|
48
54
|
|
|
49
|
-
If a token is pasted into the conversation, do not repeat or use it. Tell the user to revoke
|
|
55
|
+
If a token is pasted into the conversation, do not repeat or use it. Tell the user to revoke or regenerate it at its provider and configure the replacement locally. Check only whether the expected field is present, never its length, prefix, hash, or value.
|
|
50
56
|
|
|
51
57
|
## Locate the Fuploader CLI
|
|
52
58
|
|
|
@@ -62,6 +68,7 @@ For npm package maintenance, use `fupload update` to update the CLI and every re
|
|
|
62
68
|
- For official NewBeeBox, read [references/newbee-official-cli.md](references/newbee-official-cli.md), then run the installed `ncc docs` and exact leaf `--help`. The bundled reference is a complete official snapshot, but runtime docs win when versions differ.
|
|
63
69
|
- Read [references/newbee.md](references/newbee.md) only when the user explicitly selected the third-party Python NewBeeBox channel.
|
|
64
70
|
- Read [references/dd.md](references/dd.md) only for DD.
|
|
71
|
+
- Read [references/curseforge.md](references/curseforge.md) for every CurseForge lookup or upload.
|
|
65
72
|
- For Python, run the exact leaf command with `--help` before creating its input. Treat help as the executable schema contract.
|
|
66
73
|
|
|
67
74
|
## Investigate
|
|
@@ -70,6 +77,8 @@ Inspect the user-provided project or current workspace for `.toc`, README, chang
|
|
|
70
77
|
|
|
71
78
|
Use read-only commands from the selected channel to discover the current account's records, target detail, versions, backups, categories, game branches/builds, installation paths, life types, VIP levels, channels, and association candidates. Never ask the user to copy an ID that the selected CLI can query.
|
|
72
79
|
|
|
80
|
+
For CurseForge, run `curseforge session doctor`, then `curseforge project list` using `CURSEFORGE_AUTHOR_ID` or the user-provided non-secret `--author-id`. Select a public project by name plus `project_id`. Before a version-tagged upload, run `curseforge plugin game-versions` and select returned IDs or names; do not guess IDs from display names. A parent-file upload instead uses `parent_file_id` and omits both game-version fields. Inspect the ZIP and local release notes. An empty public list does not prove the account has no private, draft, or pending-review projects.
|
|
81
|
+
|
|
73
82
|
For official NewBeeBox, use the documented `ncc wow addons list|info|categories|versions`, `wa list|info|categories`, `uipack list|info`, `cloudbackup list|info`, and related commands needed by the action. Use the exact options returned by the installed help. `addons push` compatibility uses explicit build strings or documented `auto`; never substitute a parent branch ID. An empty required option list blocks the write.
|
|
74
83
|
|
|
75
84
|
For third-party Python NewBeeBox, use Creator Center webpage requests and form behavior as the business baseline. Always read `newbee options content-origins`, `subscribe-plans`, and `time-ranges` when the corresponding field is present. Also read plugin categories/builds, WA categories, attachment paths, and each resource's `co-author`/`reference` candidates for those fields. Empty option output blocks the write. Plugin compatibility uses build strings from `game-versions.items[].versions`, never the parent branch `id`. Preserve omitted edit fields; an explicit empty `co_authors` or `references` array replaces and clears that complete relation only after the main record readback succeeds.
|
|
@@ -96,13 +105,13 @@ For a configuration, require a cloud backup already uploaded by the matching des
|
|
|
96
105
|
|
|
97
106
|
Expose every business field writable through the selected channel. Do not silently accept a webpage preselection or invent a business default. This includes applicable game type/build, categories, origin, format, visibility, review submission, payment, lifetime, price, room/channel, synchronization, membership, associations, backup content, WTF roles, incremental selections, retail UI data, WA material mode, and install path. When official `ncc` does not expose a webpage field or action, state that capability boundary; do not guess a hidden flag or silently switch to Python.
|
|
98
107
|
|
|
99
|
-
Use existing remote values only for omitted fields in `edit` or `update`, where omission means preserve. Show candidates by human name plus ID/SN and relevant status. Ask only choices that cannot be determined from the user's explicit request, local artifacts, or remote reads. For DD, do not draft the executable JSON incrementally: close the full dependency graph first, then generate one final JSON containing parent fields and stable child IDs/selectors. Python repeats the live GETs before upload or mutation and rejects missing or cross-parent selections; detail is authoritative when DD list and detail timestamps differ. When an existing plugin or WA has `assign_user_sn`, only public scope is selectable and the final rebuilt form must remain public.
|
|
108
|
+
Use existing remote values only for omitted fields in `edit` or `update`, where omission means preserve. Show candidates by human name plus ID/SN and relevant status. Ask only choices that cannot be determined from the user's explicit request, local artifacts, or remote reads. For CurseForge upload, collect project ID, ZIP path, changelog, release type, version IDs/names or parent file choice, and each requested optional metadata field; do not infer visibility or approval from upload acceptance. For DD, do not draft the executable JSON incrementally: close the full dependency graph first, then generate one final JSON containing parent fields and stable child IDs/selectors. Python repeats the live GETs before upload or mutation and rejects missing or cross-parent selections; detail is authoritative when DD list and detail timestamps differ. When an existing plugin or WA has `assign_user_sn`, only public scope is selectable and the final rebuilt form must remain public.
|
|
100
109
|
|
|
101
110
|
## Prepare and confirm
|
|
102
111
|
|
|
103
112
|
Create a durable release directory under the target project's root, never under or beside the installed Skill. Use `publish/<YYYYMMDD-HHmmss>-<platform>-<resource>-<action>/`; if that name already exists, append `-2`, `-3`, and so on instead of reusing it. Put every atomic step in a versioned JSON file ordered as `01-<action>.json`, `02-<action>.json`, and so on. A retry or readback for the same plan reuses its directory; a new independent publishing plan gets a new directory. Use JSON, not YAML. Keep the directory after execution as the target project's publishing record, and do not change its ignore rules unless the user asks.
|
|
104
113
|
|
|
105
|
-
For Python, these are executable `--input` documents using the leaf schema. For official `ncc`, these are redacted plan records containing the channel, working directory, argument vector, non-secret business inputs, local file references, and expected readback; `ncc` does not consume them. Never store a token, raw WA string, raw configuration content, or signed URL in a plan record. Use `@file` or local path references for content. Run `--dry-run` where the selected leaf documents it, especially `ncc wow addons push --dry-run
|
|
114
|
+
For Python, these are executable `--input` documents using the leaf schema. For official `ncc`, these are redacted plan records containing the channel, working directory, argument vector, non-secret business inputs, local file references, and expected readback; `ncc` does not consume them. Never store a token, API key, raw WA string, raw configuration content, or signed URL in a plan record. Use `@file` or local path references for content. Run `--dry-run` where the selected leaf documents it, especially `ncc wow addons push --dry-run` and every CurseForge upload; do not invent dry-run support for other official commands.
|
|
106
115
|
|
|
107
116
|
Before the first write, present one complete human-readable plan containing:
|
|
108
117
|
|
|
@@ -116,12 +125,16 @@ Before the first write, present one complete human-readable plan containing:
|
|
|
116
125
|
|
|
117
126
|
Obtain one explicit confirmation for that exact plan. If the plan changes materially, confirm the changed plan once.
|
|
118
127
|
|
|
128
|
+
For CurseForge, confirmation must name the public project and numeric ID, ZIP path, selected game-version IDs/names or parent file ID, changelog, release type, optional metadata and relations, and whether manual release is requested. After confirmation, execute exactly one upload attempt. A dry-run performs local validation only and is not remote permission or ID validation.
|
|
129
|
+
|
|
119
130
|
For a Python delete, first run the resource `get` command, show the exact name and ID/SN, and obtain confirmation for that single record. NewBeeBox delete input contains only the schema, `id`, and `confirm: "DELETE"`; DD delete input contains only the schema, `sn`, and `confirm_delete: true`. Do not reuse confirmation for another record and do not retry an uncertain delete. Official `ncc` currently documents plugin, WA, and configuration main-record deletion as web-only; state that boundary and wait for an explicit third-party selection before offering the Python delete command.
|
|
120
131
|
|
|
121
132
|
## Execute and verify
|
|
122
133
|
|
|
123
134
|
Run writes serially. For official `ncc`, always request `-o json`, parse stdout as JSON, and treat stderr only as progress diagnostics. For Python, parse its stable JSON output. Never scrape human text. After each successful step, immediately run the corresponding info/get/list/versions/history command and compare the intended fields in the same DD session when applicable. DD performs a bounded GET-only readback poll and never resends a mutation during verification. For DD plugin update, use a nonempty `/addon/addon_versions` result only as a pre-upload duplicate guard; the matching author-list item's `latest_version` is the primary success confirmation and `detail_v2` is supplementary. Plugin edit also uses the same-SN author projection when detail remains stale. An empty history is diagnostic-only. Treat “submitted for review” and “under review” as distinct from “approved” or “publicly visible.”
|
|
124
135
|
|
|
136
|
+
For CurseForge, treat the Upload API's returned file ID as upload acceptance only. Record the ID and command result, then report that moderation, processing, manual release, and public visibility are separate states. On an interrupted or ambiguous upload, do not resend automatically because that can create a duplicate file; inspect the Authors project page or public file list before deciding on a new attempt. Follow the HTTP/error handling table in the CurseForge reference.
|
|
137
|
+
|
|
125
138
|
In the third-party Python NewBeeBox channel, public plugin publication is three atomic writes: create privately, upload and verify the first version, then edit to public with explicit review intent. Never send a public `share_state` during Python create. In the official channel, follow the installed `ncc docs` sequence for create, init, push, and visibility instead of applying Python wire rules.
|
|
126
139
|
|
|
127
140
|
Stop on the first failure. Report completed steps, retained IDs/SNs or media references, the redacted failure stage, whether verification is required, and the smallest safe retry. Explicit DD HTTP/business rejection has `verification_required=false`; interrupted PUT/mutation or failed readback after an accepted write has `verification_required=true`. Native DD failures may include a bounded message, HTTP status, native business code, field validation hints, and `details.log_path`. When `log_path` is present, read only the referenced Fuploader JSONL record under the DD version directory; do not inspect DD's other logs or any credential files. The Fuploader record contains sanitized request and response JSON/body plus byte counts and truncation flags, with signed URLs, cookies, JWTs, credentials, client identifiers, signatures, and tokens recursively redacted. Read back after an uncertain write before resending it. Official NewBeeBox exit codes are `0` success, `1` business error, `2` authentication failure, and `3` network error. Never loop on `quota_exceeded`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Fupload"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "使用 $fupload
|
|
3
|
+
short_description: "发布新手盒子、网易DD与CurseForge的魔兽世界作者内容"
|
|
4
|
+
default_prompt: "使用 $fupload 检查当前项目和远端状态;新手盒子优先使用官方 ncc,DD 与 CurseForge 使用 fupload CLI;整理全部业务选项并在确认后完成发布或更新。"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "fupload.v1.curseforge.plugin.upload",
|
|
3
|
+
"project_id": 123456,
|
|
4
|
+
"file": "./addon.zip",
|
|
5
|
+
"changelog": "Release notes for 1.0.0",
|
|
6
|
+
"changelog_type": "markdown",
|
|
7
|
+
"display_name": "Example AddOn 1.0.0",
|
|
8
|
+
"game_versions": [12345],
|
|
9
|
+
"game_version_names": ["Retail"],
|
|
10
|
+
"release_type": "release",
|
|
11
|
+
"relations": {
|
|
12
|
+
"projects": [
|
|
13
|
+
{
|
|
14
|
+
"slug": "example-library",
|
|
15
|
+
"project_id": 74924,
|
|
16
|
+
"type": "requiredDependency"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"is_marked_for_manual_release": false
|
|
21
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# CurseForge reference
|
|
2
|
+
|
|
3
|
+
Use this reference only for CurseForge public author-project lookup and WoW plugin ZIP upload. Fuploader uses two independent official APIs and credentials.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Capability boundary](#capability-boundary)
|
|
8
|
+
- [Local configuration](#local-configuration)
|
|
9
|
+
- [Public project lookup](#public-project-lookup)
|
|
10
|
+
- [Game versions](#game-versions)
|
|
11
|
+
- [Plugin upload](#plugin-upload)
|
|
12
|
+
- [Metadata contract](#metadata-contract)
|
|
13
|
+
- [Relations](#relations)
|
|
14
|
+
- [Plan, dry-run, and confirmation](#plan-dry-run-and-confirmation)
|
|
15
|
+
- [Success, visibility, and verification](#success-visibility-and-verification)
|
|
16
|
+
- [Errors and retry rules](#errors-and-retry-rules)
|
|
17
|
+
- [Official documentation](#official-documentation)
|
|
18
|
+
|
|
19
|
+
## Capability boundary
|
|
20
|
+
|
|
21
|
+
Fuploader supports:
|
|
22
|
+
|
|
23
|
+
- querying public World of Warcraft projects for one numeric author ID through the CurseForge Core API;
|
|
24
|
+
- listing Upload API game-version choices;
|
|
25
|
+
- uploading one ZIP to an existing World of Warcraft project.
|
|
26
|
+
|
|
27
|
+
It does not create projects, list an authenticated account's private/draft/pending-review projects, update an existing file, or delete a project/file. Create and inspect non-public projects in the [CurseForge Authors dashboard](https://authors.curseforge.com/). An empty Core API result means only that no matching public project was returned.
|
|
28
|
+
|
|
29
|
+
## Local configuration
|
|
30
|
+
|
|
31
|
+
Use `~/.fupload/curseforge.env`:
|
|
32
|
+
|
|
33
|
+
```dotenv
|
|
34
|
+
CURSEFORGE_AUTHOR_ID=
|
|
35
|
+
CURSEFORGE_API_KEY=
|
|
36
|
+
CURSEFORGE_UPLOAD_TOKEN=
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `CURSEFORGE_AUTHOR_ID`: non-secret positive integer used by public project lookup. It is an author membership/owner identifier, not an account username or project ID.
|
|
40
|
+
- `CURSEFORGE_API_KEY`: secret CurseForge for Studios/Core API key sent as `x-api-key`.
|
|
41
|
+
- `CURSEFORGE_UPLOAD_TOKEN`: secret Authors Upload API token sent as `X-Api-Token`.
|
|
42
|
+
|
|
43
|
+
The two secrets are not interchangeable. npm install/update creates the directory and template file when missing; an existing file is preserved byte for byte and is never backfilled or overwritten. Process environment values override file values for the current process. Never read, echo, log, copy into `publish/`, or request either secret in chat.
|
|
44
|
+
|
|
45
|
+
Check presence without exposing values:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
fupload curseforge session doctor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If the author ID is absent, ask for the numeric ID because it is non-secret, or use `--author-id` for that lookup. If a secret is absent, direct the user to fill the local file and rerun doctor.
|
|
52
|
+
|
|
53
|
+
## Public project lookup
|
|
54
|
+
|
|
55
|
+
Official request:
|
|
56
|
+
|
|
57
|
+
```http
|
|
58
|
+
GET https://api.curseforge.com/v1/mods/search?gameId=1&authorId=AUTHOR_ID&index=0&pageSize=50
|
|
59
|
+
Accept: application/json
|
|
60
|
+
x-api-key: CURSEFORGE_API_KEY
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`gameId=1` selects World of Warcraft. The Core API defines `authorId` as filtering mods for which that author is a member; `primaryAuthorId` is the separate owner-only filter. `index` is zero-based, `pageSize` defaults to and is capped at 50, and `index + pageSize` cannot exceed 10,000.
|
|
64
|
+
|
|
65
|
+
Fuploader commands:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
fupload curseforge project list
|
|
69
|
+
fupload curseforge project list --author-id 138844367
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `pagination.totalCount` as the count of matching public projects. Select the upload target by human-readable project name and returned numeric project ID. Do not interpret this response as a private account inventory.
|
|
73
|
+
|
|
74
|
+
## Game versions
|
|
75
|
+
|
|
76
|
+
Official request against the WoW Authors host:
|
|
77
|
+
|
|
78
|
+
```http
|
|
79
|
+
GET https://wow.curseforge.com/api/game/versions
|
|
80
|
+
X-Api-Token: CURSEFORGE_UPLOAD_TOKEN
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The response entries contain `id`, `gameVersionTypeID`, `name`, and `slug`. Use the returned IDs or official names in upload metadata.
|
|
84
|
+
|
|
85
|
+
Fuploader command:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
fupload curseforge plugin game-versions
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Fetch the current list immediately before planning an upload. Present selected names and IDs together; never derive IDs from version strings.
|
|
92
|
+
|
|
93
|
+
## Plugin upload
|
|
94
|
+
|
|
95
|
+
Official request:
|
|
96
|
+
|
|
97
|
+
```http
|
|
98
|
+
POST https://wow.curseforge.com/api/projects/{projectId}/upload-file
|
|
99
|
+
X-Api-Token: CURSEFORGE_UPLOAD_TOKEN
|
|
100
|
+
Content-Type: multipart/form-data; boundary=...
|
|
101
|
+
|
|
102
|
+
metadata=<JSON object>
|
|
103
|
+
file=<ZIP bytes>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The multipart request has the text field `metadata` and binary field `file`. The project ID comes from the existing project's overview URL/API record. On acceptance the official API returns JSON containing the new file `id`.
|
|
107
|
+
|
|
108
|
+
Fuploader accepts the strict schema `fupload.v1.curseforge.plugin.upload`:
|
|
109
|
+
|
|
110
|
+
| Field | Required | Contract |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `schema` | yes | Exactly `fupload.v1.curseforge.plugin.upload`. |
|
|
113
|
+
| `project_id` | yes | Positive integer for an existing project. |
|
|
114
|
+
| `file` | yes | Existing local ZIP path. |
|
|
115
|
+
| `changelog` | yes | Release notes string. |
|
|
116
|
+
| `changelog_type` | no | `text`, `html`, or `markdown`; official default is `text`. |
|
|
117
|
+
| `display_name` | no | Non-empty friendly file name. |
|
|
118
|
+
| `game_versions` | no | Non-empty array of numeric IDs from `plugin game-versions`; not supported with `parent_file_id`. |
|
|
119
|
+
| `game_version_names` | no | Array of non-empty official game-version names. |
|
|
120
|
+
| `release_type` | yes | `alpha`, `beta`, or `release`. |
|
|
121
|
+
| `parent_file_id` | no | Positive parent file ID; mutually exclusive with `game_versions` and `game_version_names`. |
|
|
122
|
+
| `relations` | no | Object described below. |
|
|
123
|
+
| `is_marked_for_manual_release` | no | Boolean; when true, approval does not immediately release the file. |
|
|
124
|
+
|
|
125
|
+
Compatibility fields are optional in the official API and executable schema. When supplied, use current values returned by `plugin game-versions`; do not combine `parent_file_id` with either version field. Example: [curseforge-plugin-upload.json](../examples/curseforge-plugin-upload.json).
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
fupload curseforge plugin upload --input publish\20260807-120000-curseforge-plugin-upload\01-upload.json --dry-run
|
|
129
|
+
fupload curseforge plugin upload --input publish\20260807-120000-curseforge-plugin-upload\01-upload.json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The executable Fuploader JSON uses snake_case. The provider maps it to the official camelCase Upload API metadata keys.
|
|
133
|
+
|
|
134
|
+
## Metadata contract
|
|
135
|
+
|
|
136
|
+
The official Upload API documents these metadata fields:
|
|
137
|
+
|
|
138
|
+
| Official field | Fuploader field | Meaning and constraints |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| `changelog` | `changelog` | Change description; HTML or Markdown requires matching `changelogType`. |
|
|
141
|
+
| `changelogType` | `changelog_type` | `text`, `html`, or `markdown`; optional, defaults to `text`. |
|
|
142
|
+
| `displayName` | `display_name` | Optional friendly display name. |
|
|
143
|
+
| `parentFileID` | `parent_file_id` | Optional positive parent file ID. Fuploader rejects it together with `gameVersions` or `gameVersionNames`. |
|
|
144
|
+
| `gameVersions` | `game_versions` | Optional array of numeric game-version IDs; not supported with `parentFileID`. |
|
|
145
|
+
| `gameVersionNames` | `game_version_names` | Optional array of game-version names. |
|
|
146
|
+
| `releaseType` | `release_type` | Required: `alpha`, `beta`, or `release`. |
|
|
147
|
+
| `isMarkedForManualRelease` | `is_marked_for_manual_release` | Optional manual-release flag. |
|
|
148
|
+
| `relations` | `relations` | Optional project dependency relations. |
|
|
149
|
+
|
|
150
|
+
Do not put official camelCase keys directly into Fuploader input; unknown fields are rejected. `project_id` and `file` select the endpoint/body file and are not members of the metadata JSON.
|
|
151
|
+
|
|
152
|
+
## Relations
|
|
153
|
+
|
|
154
|
+
Fuploader input mirrors the official relation shape while using snake_case for `projectID`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"relations": {
|
|
159
|
+
"projects": [
|
|
160
|
+
{
|
|
161
|
+
"slug": "related-project-slug",
|
|
162
|
+
"project_id": 74924,
|
|
163
|
+
"type": "requiredDependency"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each item requires non-empty `slug` and `type`. `project_id` is optional and, when supplied, must be a positive integer for an exact project match. Unknown relation keys are rejected. Official relation types are:
|
|
171
|
+
|
|
172
|
+
- `embeddedLibrary`
|
|
173
|
+
- `incompatible`
|
|
174
|
+
- `optionalDependency`
|
|
175
|
+
- `requiredDependency`
|
|
176
|
+
- `tool`
|
|
177
|
+
|
|
178
|
+
The wire mapping is `slug`, optional `projectID`, and `type` inside `relations.projects`. Resolve and show each intended related project during planning; do not guess a slug or ID.
|
|
179
|
+
|
|
180
|
+
## Plan, dry-run, and confirmation
|
|
181
|
+
|
|
182
|
+
Create `publish/<YYYYMMDD-HHmmss>-curseforge-plugin-upload/01-upload.json`. Store only non-secret business fields and local file paths. Never store the API key or upload token.
|
|
183
|
+
|
|
184
|
+
Run `--dry-run` before confirmation. It validates strict JSON, field values, relation structure, and local file existence without authenticating, querying remote permissions, or uploading.
|
|
185
|
+
|
|
186
|
+
Present one complete plan containing:
|
|
187
|
+
|
|
188
|
+
- author ID and public project name plus project ID;
|
|
189
|
+
- ZIP path and inspected filename;
|
|
190
|
+
- selected game-version names plus IDs, or parent file ID;
|
|
191
|
+
- release type, changelog/type, display name, relations, and manual-release value;
|
|
192
|
+
- the exact `fupload curseforge plugin upload --input ...` command;
|
|
193
|
+
- the effects boundary: acceptance is not approval, release, or public visibility.
|
|
194
|
+
|
|
195
|
+
Obtain one explicit confirmation for that exact plan. If any project, file, version, parent, relation, release type, or manual-release choice changes, present and confirm the changed plan. After confirmation, issue only one upload request.
|
|
196
|
+
|
|
197
|
+
## Success, visibility, and verification
|
|
198
|
+
|
|
199
|
+
The Upload API success response is a JSON object containing the new file ID. Record that literal ID and exit status. This verifies that CurseForge accepted the upload request, not that moderation completed or the file is publicly visible.
|
|
200
|
+
|
|
201
|
+
Visibility boundaries:
|
|
202
|
+
|
|
203
|
+
- Core project search lists public projects only and has no authenticated current-account inventory.
|
|
204
|
+
- Upload acceptance can precede processing, moderation, approval, manual release, and public visibility.
|
|
205
|
+
- `is_marked_for_manual_release=true` intentionally adds a later author release decision after approval.
|
|
206
|
+
- The public search/list response cannot verify private, draft, or pending-review state.
|
|
207
|
+
|
|
208
|
+
Fuploader does not expose an authenticated uploaded-file status/readback endpoint. Use the returned file ID as the acceptance record and inspect the Authors dashboard when a later state must be confirmed. Do not describe acceptance as publication.
|
|
209
|
+
|
|
210
|
+
## Errors and retry rules
|
|
211
|
+
|
|
212
|
+
Interpret responses conservatively:
|
|
213
|
+
|
|
214
|
+
| Result | Meaning and action |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| Local schema/file error | No request was sent. Correct the input and rerun dry-run. |
|
|
217
|
+
| `400`/`422` | Metadata, version, relation, or multipart validation was rejected. Do not retry unchanged input. |
|
|
218
|
+
| `401` | Credential missing, invalid, or expired. Regenerate/configure locally; never request it in chat. |
|
|
219
|
+
| `403` | Token lacks permission for the project/action. Recheck project ownership and token scope locally. |
|
|
220
|
+
| `404` | Host, project, or endpoint was not found. Confirm the WoW host and project ID. |
|
|
221
|
+
| `409` | Treat as a conflicting/duplicate state; inspect the project before another write. |
|
|
222
|
+
| `429` | Rate limited. Honor `Retry-After` when supplied; do not loop. |
|
|
223
|
+
| `5xx` after request transmission | Outcome may be uncertain. Inspect the Authors dashboard before retrying. |
|
|
224
|
+
| Network interruption/timeout during upload | Outcome is uncertain. Never automatically resend; check the project/file list first to prevent duplicates. |
|
|
225
|
+
|
|
226
|
+
Expose only sanitized status, error kind, stage, and provider message. Never include request headers, secret values, or raw multipart bodies in output or logs.
|
|
227
|
+
|
|
228
|
+
## Official documentation
|
|
229
|
+
|
|
230
|
+
- [CurseForge for Studios introduction](https://docs.curseforge.com/docs/curseforge-for-studios/intro)
|
|
231
|
+
- [CurseForge Core REST API](https://docs.curseforge.com/rest-api/): base URL, `x-api-key`, pagination, and `GET /v1/mods/search` including `authorId`/`primaryAuthorId`.
|
|
232
|
+
- [CurseForge Upload API](https://support.curseforge.com/en/support/solutions/articles/9000197321-curseforge-upload-api): token generation, `X-Api-Token`, `GET /api/game/versions`, multipart `POST /api/projects/{projectId}/upload-file`, metadata, relations, and success file ID.
|
|
233
|
+
- [CurseForge Authors dashboard](https://authors.curseforge.com/): project creation and non-public author state.
|
|
@@ -10,6 +10,7 @@ from typing import Any, Callable, Dict, Optional, Sequence, Tuple
|
|
|
10
10
|
|
|
11
11
|
from . import __version__
|
|
12
12
|
from .dd import DD
|
|
13
|
+
from .curseforge import CurseForge
|
|
13
14
|
from .errors import FuploadError, ValidationError
|
|
14
15
|
from .io import read_json, write_error, write_output
|
|
15
16
|
from .newbee import NewBee
|
|
@@ -186,16 +187,30 @@ def _dd_tree(platforms: argparse._SubParsersAction) -> None:
|
|
|
186
187
|
leaf = _read_leaf(wa, "categories", "List DD WA category choices for a game type.", platform="dd", resource="wa", action="categories"); leaf.add_argument("--game-type", type=_positive, required=True)
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
def _curseforge_tree(platforms: argparse._SubParsersAction) -> None:
|
|
191
|
+
root = platforms.add_parser("curseforge", help="CurseForge public project lookup and author uploads")
|
|
192
|
+
groups = root.add_subparsers(dest="resource_command", required=True)
|
|
193
|
+
session = groups.add_parser("session", help="Configuration diagnostics").add_subparsers(dest="action_command", required=True)
|
|
194
|
+
_read_leaf(session, "doctor", "Check whether the fixed CurseForge configuration fields exist without revealing their values.", platform="curseforge", resource="session", action="doctor")
|
|
195
|
+
project = groups.add_parser("project", help="Public project lookup").add_subparsers(dest="action_command", required=True)
|
|
196
|
+
leaf = _read_leaf(project, "list", "List public WoW projects for one CurseForge author ID.", platform="curseforge", resource="project", action="list")
|
|
197
|
+
leaf.add_argument("--author-id", type=_positive, help="Override CURSEFORGE_AUTHOR_ID for this lookup.")
|
|
198
|
+
plugin = groups.add_parser("plugin", help="WoW plugin versions and uploads").add_subparsers(dest="action_command", required=True)
|
|
199
|
+
_read_leaf(plugin, "game-versions", "List CurseForge Upload API game-version choices.", platform="curseforge", resource="plugin", action="game-versions")
|
|
200
|
+
_write_leaf(plugin, "curseforge", "plugin", "upload", "Upload one plugin archive to an existing CurseForge project.")
|
|
201
|
+
|
|
202
|
+
|
|
189
203
|
def build_parser() -> argparse.ArgumentParser:
|
|
190
204
|
parser = _parser(
|
|
191
205
|
prog="fupload",
|
|
192
|
-
description="Atomic World of Warcraft author publishing CLI for NewBeeBox
|
|
206
|
+
description="Atomic World of Warcraft author publishing CLI for NewBeeBox, NetEase DD, and CurseForge.",
|
|
193
207
|
epilog="All output is JSON. Write commands require versioned JSON through --input and never prompt.",
|
|
194
208
|
)
|
|
195
209
|
parser.add_argument("--version", action="version", version="%(prog)s " + __version__)
|
|
196
210
|
platforms = parser.add_subparsers(dest="platform_command", required=True)
|
|
197
211
|
_newbee_tree(platforms)
|
|
198
212
|
_dd_tree(platforms)
|
|
213
|
+
_curseforge_tree(platforms)
|
|
199
214
|
return parser
|
|
200
215
|
|
|
201
216
|
|
|
@@ -243,14 +258,14 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
243
258
|
if args.dry_run:
|
|
244
259
|
write_output(platform, operation, _dry_run_data(doc, schema.name), dry_run=True)
|
|
245
260
|
return 0
|
|
246
|
-
provider = NewBee() if platform == "newbee" else DD()
|
|
261
|
+
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else CurseForge())
|
|
247
262
|
if platform == "dd":
|
|
248
263
|
data = provider.execute_write(resource, action, doc, getattr(args, "session", None))
|
|
249
264
|
else:
|
|
250
265
|
data = provider.execute_write(resource, action, doc)
|
|
251
266
|
write_output(platform, operation, data)
|
|
252
267
|
return 0
|
|
253
|
-
provider = NewBee() if platform == "newbee" else DD()
|
|
268
|
+
provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else CurseForge())
|
|
254
269
|
if platform == "dd":
|
|
255
270
|
data = provider.execute_read(resource, action, args, getattr(args, "session", None))
|
|
256
271
|
else:
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""CurseForge public project lookup and author upload provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Mapping, Optional
|
|
10
|
+
|
|
11
|
+
from .errors import FuploadError, ValidationError
|
|
12
|
+
from .transport import json_request, multipart_request
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CORE_BASE = "https://api.curseforge.com"
|
|
16
|
+
UPLOAD_BASE = "https://wow.curseforge.com"
|
|
17
|
+
CONFIG_KEYS = (
|
|
18
|
+
"CURSEFORGE_AUTHOR_ID",
|
|
19
|
+
"CURSEFORGE_API_KEY",
|
|
20
|
+
"CURSEFORGE_UPLOAD_TOKEN",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def config_path() -> Path:
|
|
25
|
+
return Path.home() / ".fupload" / "curseforge.env"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_config(path: Optional[Path] = None) -> Dict[str, str]:
|
|
29
|
+
"""Load only the fixed CurseForge fields, with process env taking precedence."""
|
|
30
|
+
source = path or config_path()
|
|
31
|
+
values: Dict[str, str] = {}
|
|
32
|
+
if source.is_file():
|
|
33
|
+
try:
|
|
34
|
+
lines = source.read_text(encoding="utf-8-sig").splitlines()
|
|
35
|
+
except OSError as exc:
|
|
36
|
+
raise FuploadError("cannot read CurseForge configuration: %s" % exc, stage="dependency_get") from exc
|
|
37
|
+
for number, raw in enumerate(lines, 1):
|
|
38
|
+
line = raw.strip()
|
|
39
|
+
if not line or line.startswith("#"):
|
|
40
|
+
continue
|
|
41
|
+
if "=" not in line:
|
|
42
|
+
raise ValidationError("expected NAME=VALUE", path="%s:%d" % (source, number))
|
|
43
|
+
name, value = line.split("=", 1)
|
|
44
|
+
name, value = name.strip(), value.strip()
|
|
45
|
+
if name not in CONFIG_KEYS:
|
|
46
|
+
raise ValidationError("unknown CurseForge configuration field", path="%s:%d" % (source, number))
|
|
47
|
+
if name in values:
|
|
48
|
+
raise ValidationError("duplicate CurseForge configuration field", path="%s:%d" % (source, number))
|
|
49
|
+
values[name] = value
|
|
50
|
+
for name in CONFIG_KEYS:
|
|
51
|
+
environment_value = os.environ.get(name, "").strip()
|
|
52
|
+
if environment_value:
|
|
53
|
+
values[name] = environment_value
|
|
54
|
+
return values
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _required(config: Mapping[str, str], *names: str) -> None:
|
|
58
|
+
missing = [name for name in names if not config.get(name)]
|
|
59
|
+
if missing:
|
|
60
|
+
raise FuploadError(
|
|
61
|
+
"missing CurseForge configuration field(s): %s" % ", ".join(missing),
|
|
62
|
+
kind="authentication_error", stage="dependency_get",
|
|
63
|
+
details={"config_path": str(config_path()), "missing": missing},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _author_id(value: Any) -> int:
|
|
68
|
+
try:
|
|
69
|
+
result = int(value)
|
|
70
|
+
except (TypeError, ValueError) as exc:
|
|
71
|
+
raise ValidationError("author ID must be a positive integer", path="--author-id") from exc
|
|
72
|
+
if result <= 0:
|
|
73
|
+
raise ValidationError("author ID must be a positive integer", path="--author-id")
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CurseForge:
|
|
78
|
+
def __init__(self, config: Optional[Mapping[str, str]] = None) -> None:
|
|
79
|
+
self.config = dict(config) if config is not None else load_config()
|
|
80
|
+
|
|
81
|
+
def execute_read(self, resource: str, action: str, args: Any) -> Any:
|
|
82
|
+
if resource == "session" and action == "doctor":
|
|
83
|
+
return self.doctor()
|
|
84
|
+
if resource == "project" and action == "list":
|
|
85
|
+
return self.project_list(getattr(args, "author_id", None))
|
|
86
|
+
if resource == "plugin" and action == "game-versions":
|
|
87
|
+
return self.game_versions()
|
|
88
|
+
raise ValidationError("unsupported CurseForge read operation")
|
|
89
|
+
|
|
90
|
+
def execute_write(self, resource: str, action: str, doc: Mapping[str, Any]) -> Any:
|
|
91
|
+
if resource == "plugin" and action == "upload":
|
|
92
|
+
return self.upload(doc)
|
|
93
|
+
raise ValidationError("unsupported CurseForge write operation")
|
|
94
|
+
|
|
95
|
+
def doctor(self) -> Dict[str, Any]:
|
|
96
|
+
fields = [{"name": name, "present": bool(self.config.get(name))} for name in CONFIG_KEYS]
|
|
97
|
+
return {
|
|
98
|
+
"config_path": str(config_path()),
|
|
99
|
+
"fields": fields,
|
|
100
|
+
"ready": all(field["present"] for field in fields),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def project_list(self, author_id: Optional[int]) -> Dict[str, Any]:
|
|
104
|
+
_required(self.config, "CURSEFORGE_API_KEY")
|
|
105
|
+
selected = _author_id(author_id if author_id is not None else self.config.get("CURSEFORGE_AUTHOR_ID"))
|
|
106
|
+
query = urllib.parse.urlencode({"gameId": 1, "authorId": selected, "index": 0, "pageSize": 50})
|
|
107
|
+
url = CORE_BASE + "/v1/mods/search?" + query
|
|
108
|
+
payload = json_request(url, headers={"x-api-key": self.config["CURSEFORGE_API_KEY"]})
|
|
109
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("data"), list):
|
|
110
|
+
raise FuploadError("CurseForge project response did not contain a data array", kind="platform_data_error", endpoint=url)
|
|
111
|
+
pagination = payload.get("pagination") if isinstance(payload.get("pagination"), dict) else {}
|
|
112
|
+
projects = []
|
|
113
|
+
for item in payload["data"]:
|
|
114
|
+
if not isinstance(item, dict):
|
|
115
|
+
raise FuploadError("CurseForge project response contained a non-object item", kind="platform_data_error", endpoint=url)
|
|
116
|
+
projects.append({
|
|
117
|
+
"id": item.get("id"),
|
|
118
|
+
"name": item.get("name"),
|
|
119
|
+
"slug": item.get("slug"),
|
|
120
|
+
"status": item.get("status"),
|
|
121
|
+
"dateCreated": item.get("dateCreated"),
|
|
122
|
+
"dateModified": item.get("dateModified"),
|
|
123
|
+
})
|
|
124
|
+
total_count = pagination.get("totalCount")
|
|
125
|
+
if isinstance(total_count, bool) or not isinstance(total_count, int) or total_count < 0:
|
|
126
|
+
total_count = len(projects)
|
|
127
|
+
return {
|
|
128
|
+
"author_id": selected,
|
|
129
|
+
"game_id": 1,
|
|
130
|
+
"total_count": total_count,
|
|
131
|
+
"projects": projects,
|
|
132
|
+
"pagination": pagination,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def game_versions(self) -> Any:
|
|
136
|
+
_required(self.config, "CURSEFORGE_UPLOAD_TOKEN")
|
|
137
|
+
return json_request(
|
|
138
|
+
UPLOAD_BASE + "/api/game/versions",
|
|
139
|
+
headers={"X-Api-Token": self.config["CURSEFORGE_UPLOAD_TOKEN"]},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def upload(self, doc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
143
|
+
_required(self.config, "CURSEFORGE_UPLOAD_TOKEN")
|
|
144
|
+
project_id = int(doc["project_id"])
|
|
145
|
+
file_path = str(doc["file"])
|
|
146
|
+
field_names = {
|
|
147
|
+
"changelog": "changelog",
|
|
148
|
+
"changelog_type": "changelogType",
|
|
149
|
+
"display_name": "displayName",
|
|
150
|
+
"game_versions": "gameVersions",
|
|
151
|
+
"game_version_names": "gameVersionNames",
|
|
152
|
+
"release_type": "releaseType",
|
|
153
|
+
"parent_file_id": "parentFileID",
|
|
154
|
+
"is_marked_for_manual_release": "isMarkedForManualRelease",
|
|
155
|
+
}
|
|
156
|
+
metadata = {wire: doc[name] for name, wire in field_names.items() if name in doc}
|
|
157
|
+
if "relations" in doc:
|
|
158
|
+
projects = []
|
|
159
|
+
for item in doc["relations"]["projects"]:
|
|
160
|
+
relation = {"slug": item["slug"], "type": item["type"]}
|
|
161
|
+
if "project_id" in item:
|
|
162
|
+
relation["projectID"] = item["project_id"]
|
|
163
|
+
projects.append(relation)
|
|
164
|
+
metadata["relations"] = {"projects": projects}
|
|
165
|
+
url = UPLOAD_BASE + "/api/projects/%d/upload-file" % project_id
|
|
166
|
+
response = multipart_request(
|
|
167
|
+
url, file_path, file_field="file",
|
|
168
|
+
fields={"metadata": json.dumps(metadata, ensure_ascii=False, separators=(",", ":"))},
|
|
169
|
+
headers={"X-Api-Token": self.config["CURSEFORGE_UPLOAD_TOKEN"]},
|
|
170
|
+
)
|
|
171
|
+
if (
|
|
172
|
+
not isinstance(response, dict)
|
|
173
|
+
or isinstance(response.get("id"), bool)
|
|
174
|
+
or not isinstance(response.get("id"), int)
|
|
175
|
+
or response["id"] <= 0
|
|
176
|
+
):
|
|
177
|
+
raise FuploadError(
|
|
178
|
+
"CurseForge upload response did not contain a positive integer id",
|
|
179
|
+
kind="platform_data_error", endpoint=url,
|
|
180
|
+
)
|
|
181
|
+
return {
|
|
182
|
+
"file_id": response["id"],
|
|
183
|
+
"project_id": project_id,
|
|
184
|
+
"archive": Path(file_path).name,
|
|
185
|
+
"status": "uploaded",
|
|
186
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import zipfile
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Tuple
|
|
8
9
|
|
|
@@ -306,6 +307,39 @@ class Schema:
|
|
|
306
307
|
raise ValidationError("expected nonempty string or null", path="$.retail_ui_config.default_edit_mode_selector")
|
|
307
308
|
if "enable_dd_setup_wizard" in retail and not isinstance(retail["enable_dd_setup_wizard"], bool):
|
|
308
309
|
raise ValidationError("expected boolean", path="$.retail_ui_config.enable_dd_setup_wizard")
|
|
310
|
+
if self.name == "fupload.v1.curseforge.plugin.upload":
|
|
311
|
+
if not zipfile.is_zipfile(value["file"]):
|
|
312
|
+
raise ValidationError("file must be a valid ZIP archive", path="$.file")
|
|
313
|
+
if "game_versions" in value and any(isinstance(item, bool) or not isinstance(item, int) or item <= 0 for item in value["game_versions"]):
|
|
314
|
+
raise ValidationError("array must contain positive integer IDs", path="$.game_versions")
|
|
315
|
+
if "game_version_names" in value and any(not isinstance(item, str) or not item.strip() for item in value["game_version_names"]):
|
|
316
|
+
raise ValidationError("array must contain nonempty strings", path="$.game_version_names")
|
|
317
|
+
relations = value.get("relations")
|
|
318
|
+
if relations is not None:
|
|
319
|
+
if set(relations) != {"projects"}:
|
|
320
|
+
unknown = sorted(set(relations) - {"projects"})
|
|
321
|
+
message = "unknown field(s): %s" % ", ".join(unknown) if unknown else "projects is required"
|
|
322
|
+
raise ValidationError(message, path="$.relations")
|
|
323
|
+
if not isinstance(relations["projects"], list):
|
|
324
|
+
raise ValidationError("expected array", path="$.relations.projects")
|
|
325
|
+
for index, relation in enumerate((relations or {}).get("projects") or []):
|
|
326
|
+
if not isinstance(relation, dict):
|
|
327
|
+
raise ValidationError("expected object", path="$.relations.projects[%d]" % index)
|
|
328
|
+
unknown = sorted(set(relation) - {"slug", "type", "project_id"})
|
|
329
|
+
if unknown:
|
|
330
|
+
raise ValidationError("unknown field(s): %s" % ", ".join(unknown), path="$.relations.projects[%d].%s" % (index, unknown[0]))
|
|
331
|
+
if not {"slug", "type"}.issubset(relation):
|
|
332
|
+
raise ValidationError("slug and type are required", path="$.relations.projects[%d]" % index)
|
|
333
|
+
if not isinstance(relation["slug"], str) or not relation["slug"].strip():
|
|
334
|
+
raise ValidationError("expected nonempty string", path="$.relations.projects[%d].slug" % index)
|
|
335
|
+
if relation["type"] not in ("embeddedLibrary", "incompatible", "optionalDependency", "requiredDependency", "tool"):
|
|
336
|
+
raise ValidationError("unsupported relation type", path="$.relations.projects[%d].type" % index)
|
|
337
|
+
if "project_id" in relation and (isinstance(relation["project_id"], bool) or not isinstance(relation["project_id"], int) or relation["project_id"] <= 0):
|
|
338
|
+
raise ValidationError("must be a positive integer", path="$.relations.projects[%d].project_id" % index)
|
|
339
|
+
if "parent_file_id" in value:
|
|
340
|
+
for field_name in ("game_versions", "game_version_names"):
|
|
341
|
+
if field_name in value:
|
|
342
|
+
raise ValidationError("must be omitted when parent_file_id is set", path="$.%s" % field_name)
|
|
309
343
|
|
|
310
344
|
|
|
311
345
|
def f(type_name: str, **kwargs: Any) -> Field:
|
|
@@ -504,6 +538,20 @@ for _resource in ("plugin", "config", "wa"):
|
|
|
504
538
|
"confirm_delete": f("boolean", choices=(True,)),
|
|
505
539
|
}, ("sn", "confirm_delete")))
|
|
506
540
|
|
|
541
|
+
register("curseforge", "plugin", "upload", required({
|
|
542
|
+
"project_id": f("integer", minimum=1),
|
|
543
|
+
"file": f("string", local_file=True),
|
|
544
|
+
"changelog": f("string"),
|
|
545
|
+
"changelog_type": f("string", choices=("text", "html", "markdown")),
|
|
546
|
+
"display_name": f("string", nonempty=True),
|
|
547
|
+
"game_versions": f("array", nonempty=True),
|
|
548
|
+
"game_version_names": f("array"),
|
|
549
|
+
"release_type": f("string", choices=("alpha", "beta", "release")),
|
|
550
|
+
"parent_file_id": f("integer", minimum=1),
|
|
551
|
+
"relations": f("object"),
|
|
552
|
+
"is_marked_for_manual_release": f("boolean"),
|
|
553
|
+
}, ("project_id", "file", "changelog", "release_type")))
|
|
554
|
+
|
|
507
555
|
|
|
508
556
|
def get_schema(platform: str, resource: str, action: str) -> Schema:
|
|
509
557
|
try:
|
package/npm/bin/fupload.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
+
import { ensureCurseForgeEnv } from "../lib/curseforge-config.mjs";
|
|
6
7
|
import { recordManagedSkill } from "../lib/managed-install.mjs";
|
|
7
8
|
import { parseLauncherOptions, resolveSkillDirectory } from "../lib/options.mjs";
|
|
8
9
|
import { discoverPython, runPython } from "../lib/python.mjs";
|
|
@@ -52,6 +53,7 @@ async function main() {
|
|
|
52
53
|
|
|
53
54
|
let ensured;
|
|
54
55
|
try {
|
|
56
|
+
ensureCurseForgeEnv();
|
|
55
57
|
ensured = await ensureSkill({ packageRoot, target });
|
|
56
58
|
recordManagedSkill(target);
|
|
57
59
|
} catch (error) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const CURSEFORGE_ENV_TEMPLATE = [
|
|
6
|
+
"CURSEFORGE_AUTHOR_ID=",
|
|
7
|
+
"CURSEFORGE_API_KEY=",
|
|
8
|
+
"CURSEFORGE_UPLOAD_TOKEN=",
|
|
9
|
+
"",
|
|
10
|
+
].join("\n");
|
|
11
|
+
|
|
12
|
+
export function curseForgeEnvPath({ home = os.homedir() } = {}) {
|
|
13
|
+
return path.join(home, ".fupload", "curseforge.env");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ensureCurseForgeEnv({ home = os.homedir(), platform = process.platform } = {}) {
|
|
17
|
+
const filename = curseForgeEnvPath({ home });
|
|
18
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(filename, CURSEFORGE_ENV_TEMPLATE, {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
flag: "wx",
|
|
24
|
+
mode: 0o600,
|
|
25
|
+
});
|
|
26
|
+
if (platform !== "win32") {
|
|
27
|
+
fs.chmodSync(filename, 0o600);
|
|
28
|
+
}
|
|
29
|
+
return { path: filename, status: "created" };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code === "EEXIST") {
|
|
32
|
+
return { path: filename, status: "preserved" };
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/npm/lib/update.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { ensureCurseForgeEnv } from "./curseforge-config.mjs";
|
|
4
5
|
import { readManagedInstall, recordManagedSkill } from "./managed-install.mjs";
|
|
5
6
|
import { resolveSkillDirectory } from "./options.mjs";
|
|
6
7
|
import { ensureSkill, loadDistribution } from "./skill-installer.mjs";
|
|
@@ -27,6 +28,7 @@ export async function updateSelf({
|
|
|
27
28
|
home = os.homedir(),
|
|
28
29
|
runNpm = runNpmCli,
|
|
29
30
|
} = {}) {
|
|
31
|
+
const curseforgeConfig = ensureCurseForgeEnv({ home, platform });
|
|
30
32
|
const installation = resolveGlobalInstall(packageRoot, platform);
|
|
31
33
|
const current = loadDistribution(packageRoot);
|
|
32
34
|
const primary = target || resolveSkillDirectory({ env, home });
|
|
@@ -94,6 +96,7 @@ export async function updateSelf({
|
|
|
94
96
|
from_version: current.packageRecord.version,
|
|
95
97
|
to_version: updated.packageRecord.version,
|
|
96
98
|
npm_exit_status: npmResult.status,
|
|
99
|
+
curseforge_config: curseforgeConfig,
|
|
97
100
|
skills,
|
|
98
101
|
};
|
|
99
102
|
}
|
package/npm/postinstall.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
|
|
4
|
+
import { ensureCurseForgeEnv } from "./lib/curseforge-config.mjs";
|
|
4
5
|
import { recordManagedSkill } from "./lib/managed-install.mjs";
|
|
5
6
|
import { resolveSkillDirectory } from "./lib/options.mjs";
|
|
6
7
|
import { ensureSkill } from "./lib/skill-installer.mjs";
|
|
@@ -9,9 +10,11 @@ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "
|
|
|
9
10
|
const target = resolveSkillDirectory();
|
|
10
11
|
|
|
11
12
|
try {
|
|
13
|
+
const config = ensureCurseForgeEnv();
|
|
12
14
|
const result = await ensureSkill({ packageRoot, target });
|
|
13
15
|
recordManagedSkill(target);
|
|
14
16
|
process.stdout.write(`Fuploader Skill ${result.status}: ${target}\n`);
|
|
17
|
+
process.stdout.write(`CurseForge configuration ${config.status}: ${config.path}\n`);
|
|
15
18
|
} catch (error) {
|
|
16
19
|
process.stderr.write(`Fuploader Skill installation failed: ${error.message}\n`);
|
|
17
20
|
process.exitCode = 1;
|
package/npm/skill-manifest.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "fupload.npm-skill-manifest.v1",
|
|
3
3
|
"package_name": "@follenfang/fupload",
|
|
4
|
-
"package_version": "0.0.
|
|
5
|
-
"skill_version": "0.0.
|
|
6
|
-
"tree_sha256": "
|
|
4
|
+
"package_version": "0.0.2",
|
|
5
|
+
"skill_version": "0.0.2",
|
|
6
|
+
"tree_sha256": "4ea99f3fb2dee23792450b0d890dcd4520aa8ed827edddf6c6353a8cd753d32e",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
10
|
-
"bytes":
|
|
11
|
-
"sha256": "
|
|
10
|
+
"bytes": 342,
|
|
11
|
+
"sha256": "473872fbc6fcecb2030fd2ac66589afd72f813ad47b2e6e968ee2bca4b7e6ab3"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "examples/curseforge-plugin-upload.json",
|
|
15
|
+
"bytes": 515,
|
|
16
|
+
"sha256": "624cc03b0d6debd842270d129817904aadff67b6d462f8d1481fc95e999c053e"
|
|
12
17
|
},
|
|
13
18
|
{
|
|
14
19
|
"path": "examples/dd-config-delete.json",
|
|
@@ -70,6 +75,11 @@
|
|
|
70
75
|
"bytes": 168,
|
|
71
76
|
"sha256": "40491e9aeddc71a4e9578264e5c42b40613e0ea5f0321da29e170b84ff9f1905"
|
|
72
77
|
},
|
|
78
|
+
{
|
|
79
|
+
"path": "references/curseforge.md",
|
|
80
|
+
"bytes": 11917,
|
|
81
|
+
"sha256": "2dd0a2429a37af30c4f85a884c2e8ea6fcdb1691c34ccc0c34925890e4e60d92"
|
|
82
|
+
},
|
|
73
83
|
{
|
|
74
84
|
"path": "references/dd.md",
|
|
75
85
|
"bytes": 16543,
|
|
@@ -93,12 +103,17 @@
|
|
|
93
103
|
{
|
|
94
104
|
"path": "scripts/fupload_cli/__init__.py",
|
|
95
105
|
"bytes": 49,
|
|
96
|
-
"sha256": "
|
|
106
|
+
"sha256": "72609f840f9864ae70d3ee5226e29dc260e132e06ea4c89b0174b3ba9313d95e"
|
|
97
107
|
},
|
|
98
108
|
{
|
|
99
109
|
"path": "scripts/fupload_cli/cli.py",
|
|
100
|
-
"bytes":
|
|
101
|
-
"sha256": "
|
|
110
|
+
"bytes": 23794,
|
|
111
|
+
"sha256": "3c01585b50e1db308444d094e60751491667ea05a98e5a51f284f098b7a8f376"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"path": "scripts/fupload_cli/curseforge.py",
|
|
115
|
+
"bytes": 7959,
|
|
116
|
+
"sha256": "c607c6caba5c551a7e3970bd152de94ab1e96350ee73969048f5aea5bf42c75d"
|
|
102
117
|
},
|
|
103
118
|
{
|
|
104
119
|
"path": "scripts/fupload_cli/dd_broker.py",
|
|
@@ -137,8 +152,8 @@
|
|
|
137
152
|
},
|
|
138
153
|
{
|
|
139
154
|
"path": "scripts/fupload_cli/schema.py",
|
|
140
|
-
"bytes":
|
|
141
|
-
"sha256": "
|
|
155
|
+
"bytes": 35113,
|
|
156
|
+
"sha256": "388c89131bf032428d2ed6c047b52c54adf2fa9d1db91833caefe7a1296acf58"
|
|
142
157
|
},
|
|
143
158
|
{
|
|
144
159
|
"path": "scripts/fupload_cli/transport.py",
|
|
@@ -157,8 +172,8 @@
|
|
|
157
172
|
},
|
|
158
173
|
{
|
|
159
174
|
"path": "SKILL.md",
|
|
160
|
-
"bytes":
|
|
161
|
-
"sha256": "
|
|
175
|
+
"bytes": 21764,
|
|
176
|
+
"sha256": "ea2019f79aca3a6420aff657fa29b75fd731fc0b3fa4941068e0cfce0921afba"
|
|
162
177
|
}
|
|
163
178
|
]
|
|
164
179
|
}
|