@follenfang/fupload 0.0.11 → 0.0.15

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/fupload/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: fupload
3
3
  description: Explicit author-publishing workflow for World of Warcraft plugins, configuration shares, and WA/strings on NewBeeBox, NetEase DD, CurseForge, Heybox Workshop, and ModUs.Creator, including local Creator login reuse and plugin ZIP publishing. 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, Heybox, ModUs, plugins, configurations, or WA.
4
4
  metadata:
5
- version: "0.0.11"
5
+ version: "0.0.15"
6
6
  ---
7
7
 
8
8
  # Fupload
@@ -19,6 +19,11 @@ game/data/config/detail` with a JSON `{"keys": [...]}` body. Repeat `--key`
19
19
  to request selected server-defined configuration keys; at least one key is
20
20
  required because the service rejects an empty list.
21
21
 
22
+ Main-client form choices are available through `modus options config-tags`,
23
+ `modus options wa-tags`, and `modus options wa-support-addons`. These commands
24
+ read the official ModUs static client-data origin without sending the local
25
+ login token.
26
+
22
27
  ## Project writes
23
28
 
24
29
  `modus project create|edit|delete` uses versioned JSON through `--input`.
@@ -30,31 +35,42 @@ The complete project model includes `project_id`, `name`, `alt_name`, `summary`,
30
35
 
31
36
  ### Project field contract
32
37
 
33
- | CLI/state field | Creator wire/readback | JSON type | Required/default | Source and state dependency | Successful readback |
34
- | --- | --- | --- | --- | --- | --- |
35
- | `schema` | none | string | required by every write document | versioned CLI envelope | not sent |
36
- | `project_id` | `projectId` | positive integer | edit/delete required; create assigned by server | outer write document, not form state | `projectId` |
37
- | `game` | none confirmed | object or non-empty string | required; no default | `choose_game`; object needs a stable ID/key | preserved in state snapshot |
38
- | `name` | `name` | non-empty string | required; no default | `basic_info` | `name` |
39
- | `alt_name` | `altName` | string/null | optional; omitted preserves value | `basic_info`; edit clear uses `<null>` | `altName` |
40
- | `summary` | `summary` | non-empty string | required; no default | `basic_info` | `summary` |
41
- | `categories` | comma-delimited `categories` | 1-5 positive integer IDs | required; no default | service `categories` enum; `basic_info`; ID 998 requires BigFoot only | comma-delimited `categories` |
42
- | `publish_platforms` | derived `synchronizationType` | non-empty unique enum array | required; no default | `basic_info`; values `modus`, `bigfoot`, or both | integer `1`, `2`, or `3` |
43
- | `synchronization_type` | `synchronizationType` | integer | derived; caller value is replaced | derived from platform toggles in `basic_info` | `synchronizationType` |
44
- | `required_tier_id` | `requiredTierId` | null for this account | defaults to null | service `subscription-tiers` enum; current result is `[]`; BigFoot requires null | `requiredTierId: null` |
45
- | `repo_url` | `repoUrl` | string/null | optional; omitted preserves value | `basic_info`; create empty is omitted; edit clear uses `<null>` | `repoUrl` |
46
- | `logo_base64` | `screenshotBase64sReqs.screenshotBase64s` | base64 string | optional; default empty string | create-only `basic_info`; sent as `logo.webp` | server-managed `logo` path |
47
- | `screenshot_base64s` | same create logo payload | string array | compatibility alias; default empty array | create-only `basic_info`; first value is used when `logo_base64` is absent | server-managed `logo` path |
48
- | `license.type` | `license` JSON string `type` | non-empty string | required; no default | separate `license` step | parsed `license` |
49
- | `license.holder/year/content` | fields inside `license` JSON string | non-empty strings | optional except custom content | separate `license` step; custom requires content | parsed `license` |
50
- | `description` | `description` | string/null | edit-only; omitted preserves value | edit `basic_info`; clear uses `<null>` | `description` |
51
- | `required_dependencies` | `requiredDependencies` | string/null | edit-only; omitted preserves value | edit `basic_info`; clear uses `<null>`; dependency query is dynamic | `requiredDependencies` |
52
- | `images` | `images` | non-negative integer | edit-only; required with `image_ops` | edit `basic_info` | `images` count |
53
- | `image_ops` | `imagesOps` | operation array | edit-only; no default | edit `basic_info`; upload requires base64, delete forbids it | `images` count |
54
- | `cf_url` | `cfUrl` | string/null | read-only | known detail field may be preserved in a snapshot but is never sent | `cfUrl` |
55
- | `logo` | `logo` | string/object/null | read-only | server-managed detail field; create uses `logo_base64` instead | `logo` |
56
- | `status` | `status` | integer/null | read-only | server-managed detail field | `status` |
57
- | `project_state` | none | object | required and must be `complete` | wraps `choose_game -> basic_info -> license`; unknown nested fields are rejected | local resumable snapshot |
38
+ The table below is the A2 field inventory. `C` means create, `U` update/edit,
39
+ and `D` delete. The current CLI's Creator endpoints are `C POST
40
+ /game/data/author/project/release`, `U POST /game/data/author/project/update`,
41
+ `D POST /game/data/author/project/delete/project/{projectId}`, and readback is
42
+ `GET /game/data/author/project/detail/{projectId}` unless stated otherwise.
43
+
44
+ | CLI/state field | Creator wire name | JSON type | Required/default | Enum/source | State/linkage rule | Write endpoint | Detail/readback location |
45
+ | --- | --- | --- | --- | --- | --- | --- | --- |
46
+ | `schema` | not sent | string | required for each CLI write | schema registry | must match action | local validation only | not applicable |
47
+ | `project_id` | `projectId` | positive integer | U/D required; C server-assigned | project list/detail | outer document, not `project_state` | U/D | `projectId` |
48
+ | `game` | not sent by current CLI | object or non-empty string | required; no default | Creator game selector | first `choose_game` state; stable key/ID required | local state only | `project_state.game`; server version binding is separate |
49
+ | `name` | `name` | non-empty string, max 120 | required; no default | free text | `basic_info` | C/U | `name` |
50
+ | `alt_name` | `altName` | string/null, max 120 | optional; omitted preserves | free text | U clear becomes literal `<null>` | C/U | `altName` |
51
+ | `summary` | `summary` | non-empty string, max 500 | required; no default | free text | `basic_info` | C/U | `summary` |
52
+ | `categories` | comma-delimited `categories` | array of 1-5 positive IDs | required; no default | `GET /plugin/list/Categories` | ID `998` forces BigFoot-only | C/U | comma-delimited `categories` / category detail |
53
+ | `publish_platforms` | derives `synchronizationType` | unique array | required; no default | fixed `modus`, `bigfoot` | at least one; `[modus]=>1`, `[bigfoot]=>2`, both `=>3` | C/U through derived field | `synchronizationType` |
54
+ | `synchronization_type` | `synchronizationType` | integer `1..3` | derived; caller value replaced | derived from `publish_platforms` | must equal platform selection | C/U | `synchronizationType` |
55
+ | `required_tier_id` | `requiredTierId` | positive integer/null | default none: C omits; U null sends `<null>` | `GET /user/author/subscription/tiers` | BigFoot branch requires none | C/U | `requiredTierId`; absence is no-tier |
56
+ | `repo_url` | `repoUrl` | string/null, max 500 | optional | free URL text | C empty omitted; U clear sends `<null>` | C/U | `repoUrl` |
57
+ | `logo_base64` | `screenshotBase64sReqs.screenshotBase64s` | non-empty base64 string | C optional; default `""` | local image | C only; companion name fixed `logo.webp` | C | server-managed `logo` path |
58
+ | `screenshot_base64s` | same create logo member | base64 string array | compatibility alias; default `[]` | local images | current CLI consumes only first item when `logo_base64` absent | C | server-managed `logo` path |
59
+ | `license.type` | JSON-string `license.type` | non-empty string | required | Creator license selector | final `license` state | C/U | parse JSON-string `license` |
60
+ | `license.holder` | JSON-string `license.holder` | non-empty string | optional | free text | license step | C/U | parse JSON-string `license.holder` |
61
+ | `license.year` | JSON-string `license.year` | non-empty string | optional | free text | license step | C/U | parse JSON-string `license.year` |
62
+ | `license.content` | JSON-string `license.content` | non-empty string | custom license requires content | free text | license step/custom branch | C/U | parse JSON-string `license.content` |
63
+ | `description` | `description` | string/null, max 100000 | U only; omitted preserves | rich-text editor | clear sends `<null>` | U | `description` |
64
+ | `required_dependencies` | `requiredDependencies` | string/null, max 4000 | U only; omitted preserves | `POST /game/data/author/project/dependency/query` | clear sends `<null>` | U | `requiredDependencies` |
65
+ | `images` | `images` | non-negative integer | U required with `image_ops` | current detail count | count must agree with requested operations | U | `images` |
66
+ | `image_ops[].op` | `imagesOps[].op` | enum string | U operation required | fixed `upload`, `delete`, `rename` | upload/delete use `name`; rename uses `from/to` | U | `images`, logo/image paths |
67
+ | `image_ops[].name` | `imagesOps[].name` | non-empty string | upload/delete required | current object name | forbidden for rename | U | resulting image path/count |
68
+ | `image_ops[].base64` | `imagesOps[].base64` | base64 string | upload required; delete forbidden | local image | upload only | U | resulting image path/count |
69
+ | `image_ops[].from/to` | `imagesOps[].from/to` | non-empty strings | rename required | current/target name | rename only | U | resulting image paths |
70
+ | `cf_url` | `cfUrl` | string/null | read-only | server | never sent | none | `cfUrl` |
71
+ | `logo` | `logo` | string/object/null | read-only | server | C uses base64 input instead | none | `logo` |
72
+ | `status` | `status` | integer/null | read-only | server status enum | delete/publication state | none | `status` |
73
+ | `project_state` | not sent | object | required; state=`complete` | CLI state machine | exact order `choose_game -> basic_info -> license`; unknown fields rejected | local validation only | persisted CLI snapshot |
58
74
 
59
75
  Create-only, edit-only, and read-only fields may be preserved in a resumable
60
76
  snapshot, but `_project_wire` sends only fields supported by the selected API
@@ -63,7 +79,7 @@ before changing state.
63
79
  `publish_platforms` is a non-empty array containing `modus`,
64
80
  `bigfoot`, or both; the two values are not mutually exclusive. Creator derives
65
81
  wire `synchronizationType` as 1 for ModUs, 2 for BigFoot, and 3 for both.
66
- When BigFoot is selected, `required_tier_id` must be null. Category ID 998 is
82
+ When BigFoot is selected, `required_tier_id` must be null or omitted. Category ID 998 is
67
83
  BigFoot-exclusive and forces BigFoot as the only platform. `categories` contains
68
84
  one to five positive integer IDs.
69
85
 
@@ -77,15 +93,17 @@ named `logo.webp`; `screenshot_base64s` remains the compatibility alias.
77
93
  Edit sends only changed Creator fields plus `projectId`. `description`, cleared
78
94
  `repo_url`, cleared `alt_name`, null `required_tier_id`, and cleared
79
95
  `required_dependencies` use Creator's literal `<null>` marker. `image_ops` maps
80
- to `imagesOps` entries with `op` (`upload` or `delete`), `name`, and upload-only
81
- `base64`; `images` is required with image operations. `cf_url`, returned `logo`,
96
+ to `imagesOps` entries with `op` (`upload`, `delete`, or `rename`), `name` and
97
+ upload-only `base64`, or rename-only `from`/`to`; `images` is required with
98
+ image operations. `cf_url`, returned `logo`,
82
99
  and `status` are read-only/server-managed. `publish_platforms` and `game` are
83
100
  local form state, not project update keys.
84
101
  `project_state` is a resumable `choose_game` -> `basic_info` -> `license` ->
85
102
  `complete` snapshot and is required for both create and edit; incomplete or
86
103
  missing snapshots are rejected before dry-run or network access. Each step preserves
87
- its state on validation failure. `required_tier_id` is null for the explicit
88
- no-tier branch or a positive dropdown ID.
104
+ its state on validation failure. `required_tier_id` is null or omitted for the
105
+ no-tier branch and is omitted from the HTTP payload; a selected tier is a
106
+ positive dropdown ID.
89
107
  The write document also accepts `confirm` for destructive operations.
90
108
  Deletion requires `confirm: "DELETE"`.
91
109
 
@@ -95,6 +113,35 @@ Deletion requires `confirm: "DELETE"`.
95
113
  `project_id`, `file_id`, `version`, `type`, `supported_game_versions`, `md5`,
96
114
  `zip_size`, `unzip_size`, `path`, `toc_version`, `changelog`, and `file`.
97
115
  `transaction_log` optionally selects the redacted transaction record path.
116
+
117
+ ### Creator release/version field contract
118
+
119
+ This is the A4 inventory. Metadata create uses `POST
120
+ /game/data/author/project/upload`; metadata update/edit uses `POST
121
+ /game/data/author/project/file/update`; delete uses `POST
122
+ /game/data/author/project/delete`; detail is `GET
123
+ /game/data/author/project/file/detail/{fileId}` and history/list is `POST
124
+ /game/data/author/project/file/list`. File-ID allocation and the binary object
125
+ PUT are separate stages.
126
+
127
+ | CLI field | Creator wire name | JSON type | Required/default | Enum/source | State/linkage rule | Write endpoint | Detail/readback location |
128
+ | --- | --- | --- | --- | --- | --- | --- | --- |
129
+ | `schema` | not sent | string | required | schema registry | must match release action | local validation | not applicable |
130
+ | `project_id` | `projectId` | positive integer | all release writes required | project detail | parent project must exist | metadata create/update/delete; signature path | `projectId` in list/detail |
131
+ | `file_id` | `fileId` | positive integer | update/edit/delete required; upload allocates | `GET /game/data/author/project/fileId/{projectId}` | allocated ID is used by signature; create metadata omits it | update/delete/signature | `id`/`fileId` in release detail |
132
+ | `version` | `version` | non-empty string, max 120 | create/upload required after ZIP preflight | ZIP `.toc` plus caller label | caller value preserved | metadata create/update | `version` |
133
+ | `type` | `type` | non-empty string, max 40 | create/upload required | Creator release-type choices/API | must be a server-supported value | metadata create/update | `type` |
134
+ | `supported_game_versions` | `supportedGameVersionsReqs[]` | non-empty object array | required; ZIP-derived | installed `.toc` `Interface` values and game config | each entry requires string `gameVersion` and `server`; explicit caller value must match ZIP | metadata create/update | `supportedGameVersionsReqs`/version compatibility rows |
135
+ | `md5` | `md5` | hex string, max 64 | derived from exact ZIP bytes | local ZIP preflight | must describe bytes later PUT | metadata create/update | `md5` |
136
+ | `zip_size` | `zipSize` | non-negative integer | derived | local ZIP stat | must describe exact ZIP | metadata create/update | `zipSize` |
137
+ | `unzip_size` | `unzipSize` | non-negative integer | derived | ZIP central directory | bounded by ZIP preflight | metadata create/update | `unzipSize` |
138
+ | `path` | `path` | string, max 500 | metadata optional | Creator/object response | local path is never binary upload | metadata create/update | `path` |
139
+ | `toc_version` | `tocVersion` | string, max 80 | derived | every ZIP `.toc` `Interface` | incompatible/missing/mixed values fail before write | metadata create/update | `tocVersion` |
140
+ | `changelog` | `changelog` | string/null, max 10000 | optional | free text | omitted/empty behavior is action-specific | metadata create/update | `changelog` |
141
+ | `file` | not in JSON; raw bytes | local path | create/upload required | local filesystem | valid ZIP, max 200 MiB; bytes PUT only after metadata/signature | signed URL from `GET .../upload/signature/{projectId}/{fileId}`, then HTTP `PUT` | object upload status plus release detail hashes/sizes |
142
+ | `transaction_log` | not sent | local path string | optional | caller | redacted durable stage record | local record only | transaction JSON |
143
+ | derived signature URL | not returned to caller | URL | required for PUT | signature endpoint | query/userinfo redacted and never persisted | object-store `PUT application/zip` | HTTP status only; then release detail |
144
+
98
145
  Successful records include the completed `file_id`, metadata, signature, and
99
146
  binary upload stages that ran; failed records include `failed_stage`, a redacted
100
147
  error, and whether the ZIP was retained. The transaction record is established
@@ -111,3 +158,201 @@ never substituted for the binary upload. Release deletion requires
111
158
 
112
159
  All write commands support `--dry-run`, which validates the versioned JSON and
113
160
  local files without constructing an authenticated provider or making a request.
161
+
162
+ ## Main ModUs client: configuration shares
163
+
164
+ The installed main client stores its Chromium session under `%APPDATA%\modus\Local Storage\leveldb`.
165
+ Fupload reads the persisted `token`/`deviceId` pair and calls the fixed
166
+ `https://app.modus.cool/api` origin with the raw `Authorization` value and
167
+ `X-Device-Id`; values are never printed. The Creator DPAPI store remains a
168
+ separate authentication source for `project` and `plugin` commands.
169
+
170
+ The configuration surface uses:
171
+
172
+ * `GET /system/user/backup/list`, `GET /system/user/backup/detail/{id}`
173
+ * `POST /system/user/backup/update`, `DELETE /system/user/backup/delete/{id}`
174
+ * `POST /system/user/share/list`, `POST /system/user/share/detail`
175
+ * `POST /system/user/share/create`, `PUT /system/user/share/update`,
176
+ `DELETE /system/user/share/delete/{id}`
177
+
178
+ ### Configuration list/filter contract
179
+
180
+ | CLI field | Wire/header name | JSON type | Required/default | Enum/source | State/linkage rule | Request endpoint | Readback location |
181
+ | --- | --- | --- | --- | --- | --- | --- | --- |
182
+ | `page_num` | `pageNum` | positive integer | default `1` | caller | list only | `POST /system/user/share/list` | pagination response |
183
+ | `page_size` | `pageSize` | positive integer | default `20` | caller | list only | same | pagination response |
184
+ | `server_type` / `server` | body `server`; header `X-Server-Type` | integer | local selected Build, fallback `0` | fixed Build table below | body and header must be identical | list; header on all config calls | list/detail records by Build |
185
+ | `mine` | `mine` | boolean | default `false` | fixed boolean | list only | list | returned rows |
186
+ | `share_type` | `shareType` | non-negative integer | default `0` | service/UI | list filter | list | returned rows |
187
+ | `platform` | `platform` | integer | list default `0` | service; write choices `1`,`3` | not Build | list | `platform` |
188
+ | `keyword` | `keyword` | string | optional | caller | list filter | list | returned rows |
189
+ | `status` | `status` | integer | optional | service status values | list filter | list | `status` |
190
+ | `tags` | `tags` | string | optional | `config-tags` static rows | list filter | list | tags on rows/detail |
191
+ | `order_by` | `orderBy` | service value | optional | service/UI | list sort | list | result order |
192
+ | `is_public` | `isPublic` | integer `0/1` | optional | fixed boolean | list filter | list | `isPublic` |
193
+ | `is_paid` | `isPaid` | integer `0/1` | optional | fixed boolean | list filter | list | `isPaid` |
194
+
195
+ ### Configuration write field contract
196
+
197
+ This is the A6 inventory. Create is `POST /system/user/share/create`, update/edit
198
+ is `PUT /system/user/share/update`, delete is `DELETE
199
+ /system/user/share/delete/{id}`, and readback is `POST
200
+ /system/user/share/detail` with `{"shareIds":[id]}`. Every call carries the
201
+ selected Build in `X-Server-Type`.
202
+
203
+ | CLI field | Wire name | JSON type | Required/default | Enum/source | State/linkage rule | Write endpoint | Detail/readback location |
204
+ | --- | --- | --- | --- | --- | --- | --- | --- |
205
+ | `schema` | not sent | string | required | schema registry | must match action | local validation | not applicable |
206
+ | `share_id` | `id` / path ID | opaque decimal string | U/D required; C server-assigned | create response/detail | preserve as string | U/D | detail `id` |
207
+ | `addons_id` | `addonsId` | string, max 10000 | C required | selected backup `knownAddons.projectids` | must correspond to backup selection | C/U | `addonsId` |
208
+ | `backup_id` | `backupId` | positive integer | C required | `GET /system/user/backup/list` | account/role choices derive from this backup | C/U | `backupId` |
209
+ | `account_name` | `accountName` | string/null, max 120 | conditional | selected backup `wtfAccounts` | required and non-empty when `exclude_wtf=0`; empty when `1` | C/U | `accountName` |
210
+ | `role_name` | `roleName` | string/null, max 120 | optional in schema; UI requires selection when chosen account has roles | backup account roles | empty when `exclude_wtf=1` | C/U | `roleName` |
211
+ | `exclude_wtf` | `excludeWtf` | integer `0/1` | C required; provider normalizes missing U to `0` | fixed boolean | `1` forbids account/role; `0` requires account | C/U | `excludeWtf` |
212
+ | `content` | `content` | non-empty HTML string | C required | rich-text editor | editor image keys are uploaded before submit | C/U | `content` hash/length, not raw evidence |
213
+ | `content_text` | `contentText` | non-empty string | C required; trimmed length >20 | derived editor plain text | must correspond to `content` | C/U | `contentText` hash/length |
214
+ | `image_url` | `imageUrl` | non-empty comma-delimited key string, max 1000 | C required; 1-10 images in 1.2.16 UI | `modus media upload` results | trim/filter keys; first is cover; delete/reorder updates CSV, no blob-delete API observed | C/U | `imageUrl` |
215
+ | `is_paid` | `isPaid` | integer `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `isPaid` |
216
+ | `price` | `price` | number `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `price` |
217
+ | `is_public` | `isPublic` | integer `0/1` | CLI optional; 1.2.16 form defaults public | fixed boolean | visibility/soft-delete state | C/U | `isPublic` |
218
+ | `share_type` | `shareType` | integer `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `shareType` |
219
+ | `tags` | comma-delimited `tags` | string of 1-3 IDs | C required | `modus options config-tags` | no empty/duplicate positions | C/U | `tags` and/or `modusShareTags` |
220
+ | `title` | `title` | string, max 120 | C required; trimmed length >=6 | free text | none | C/U | `title` |
221
+ | `required_tier_id` | `requiredTierId` | positive integer/null | default none: omitted | normal subscription or season option | selected tier disables BigFoot platform; ID from active options | C/U | `requiredTierId`; absence means none |
222
+ | `sub_type` | `subType` | integer `0/1` | helper default `0` | fixed normal=`0`, season=`1` | determines tier option source | C/U | `subType` |
223
+ | `platform` | `platform` | integer `1/3` | provider C/U default `1` | ModUs=`1`, ModUs+BigFoot=`3` | at least ModUs; tier selection forces `1` | C/U | `platform` |
224
+ | `synchronization_type` | `synchronizationType` | integer `1/3` | C defaults from platform; U omitted unless explicit | derived from platform | C should equal platform; 1.2.16 config U form omits it | C/U | `synchronizationType` if returned |
225
+ | `server_type` | header `X-Server-Type` | integer `0..4` | local selected Build or fallback `0` | fixed Build table | not `platform`; applies to target namespace | C/U/D/detail | record returned under same Build |
226
+ | `confirm` | not sent | literal `"DELETE"` | D required | fixed CLI guard | destructive action only | local guard then D | detail `status=4,isPublic=0`; absent from active list |
227
+
228
+ Backup rename uses `backup_id`, `backup_name`, and `server_type`; backup
229
+ deletion uses `backup_id`, `confirm`, and `server_type`.
230
+
231
+ ## Main ModUs client: strings
232
+
233
+ String articles use:
234
+
235
+ * `POST /system/user/import/list`, `POST /system/user/import/detail`
236
+ * `POST /system/user/import/create`, `POST /system/user/import/update`
237
+ * `DELETE /system/user/import/delete/{id}`
238
+ * `POST /system/user/import/version/publish`
239
+ * `DELETE /system/user/import/version/delete?versionId={id}`
240
+
241
+ ### WA/string list/filter contract
242
+
243
+ | CLI field | Wire/header name | JSON type | Required/default | Enum/source | State/linkage rule | Request endpoint | Readback location |
244
+ | --- | --- | --- | --- | --- | --- | --- | --- |
245
+ | `page_num` | `pageNum` | positive integer | default `1` | caller | list only | `POST /system/user/import/list` | pagination response |
246
+ | `page_size` | `pageSize` | positive integer | default `10` | caller | list only | same | pagination response |
247
+ | `server_type` / `server` | body `server`; header `X-Server-Type` | integer | local Build, fallback `0` | fixed Build table | body/header must match | list; header on every WA call | list/detail records by Build |
248
+ | `mine` | `mine` | boolean | default `false` | fixed boolean | list only | list | rows |
249
+ | `status` | `status` | integer | default `1` | service status enum | list only | list | `status` |
250
+ | `platform` | `platform` | integer | list default `0` | service; write `1/3` | not Build | list | `platform` |
251
+ | `keyword` | `keyword` | string | optional | caller | list filter | list | rows |
252
+ | `support_addon` | `supportAddon` | string | optional | `wa-support-addons` static rows | list filter | list | `supportAddon` |
253
+ | `tags` | `tags` | string | optional | `wa-tags` static rows | list filter | list | tags on rows/detail |
254
+ | `is_paid` | `isPaid` | integer `0/1` | optional | fixed boolean | list filter | list | `isPaid` |
255
+ | `order_by` | `orderBy` | service value | optional | service/UI | list sort | list | result order |
256
+
257
+ ### WA/string write field contract
258
+
259
+ This is the A8 inventory. Create is `POST /system/user/import/create`,
260
+ update/edit is `POST /system/user/import/update`, delete is `DELETE
261
+ /system/user/import/delete/{id}`, and readback is `POST
262
+ /system/user/import/detail` with `{"importIds":[id]}`.
263
+
264
+ | CLI field | Wire name | JSON type | Required/default | Enum/source | State/linkage rule | Write endpoint | Detail/readback location |
265
+ | --- | --- | --- | --- | --- | --- | --- | --- |
266
+ | `schema` | not sent | string | required | schema registry | must match action | local validation | not applicable |
267
+ | `import_id` | `id` / path ID | opaque decimal string | U/D required; C server-assigned | create response/detail | preserve as string | U/D | detail `id` |
268
+ | `support_addon` | `supportAddon` | non-empty string, max 120 | C required | `modus options wa-support-addons` | chosen name must resolve to an addon row | C/U | `supportAddon` |
269
+ | `addons_id` | `addonsId` | non-empty string, max 10000 | C required | ID on selected support-addon row | must match `support_addon` | C/U | `addonsId` |
270
+ | `code_text` | `codeText` | non-empty string | C required; version publish required | WA/string input | create version and later version payload must be deliberate | C/U/version publish | current top-level `codeText` is not echoed; after the next publish, the previous current value is read from `versionList[].codeText` |
271
+ | `version` | `version` | non-empty string, max 120 | C required; U form preserves current | free text/version list | new immutable versions use version-publish | C/U/version publish | `version`, `versionList[].version` |
272
+ | `content` | `content` | non-empty HTML string | C required | rich-text editor | editor images uploaded before submit | C/U | `content` hash/length |
273
+ | `content_text` | `contentText` | non-empty string | C required; trimmed length >20 | derived plain text | must correspond to HTML content | C/U | `contentText` hash/length |
274
+ | `file_path` | `filePath` | literal empty string | optional in CLI; UI fixed `""` | fixed client behavior | only `""` accepted | C/U | `filePath` |
275
+ | `image_url` | `imageUrl` | non-empty comma-delimited key string, max 1000 | C required; 1-10 images in 1.2.16 UI | `modus media upload` results | first key is cover; delete/reorder rewrites CSV, no blob-delete API observed | C/U | `imageUrl` |
276
+ | `tags` | comma-delimited `tags` | string of 1-3 IDs | C required | `modus options wa-tags` | no empty positions | C/U | `tags` |
277
+ | `title` | `title` | string, max 120 | C required; trimmed length >=6 | free text | none | C/U | `title` |
278
+ | `is_paid` | `isPaid` | integer `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `isPaid` |
279
+ | `price` | `price` | number `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `price` |
280
+ | `is_public` | `isPublic` | integer `0/1` | CLI optional; 1.2.16 form fixes `1` | fixed boolean | soft-delete later forces private | C/U | `isPublic` |
281
+ | `share_type` | `shareType` | integer `0` | CLI optional; 1.2.16 form fixes `0` | installed client fixed value | nonzero values fail before mutation | C/U | `shareType` |
282
+ | `required_tier_id` | `requiredTierId` | positive integer/null | default none: omitted | normal/season tier options | selected tier disables BigFoot | C/U | `requiredTierId`; absence means none |
283
+ | `sub_type` | `subType` | integer `0/1` | helper default `0` | normal=`0`, season=`1` | selects tier option source | C/U | `subType` |
284
+ | `platform` | `platform` | integer `1/3` | C/U default `1` | ModUs=`1`, ModUs+BigFoot=`3` | at least ModUs; tier selection forces `1` | C/U | `platform` |
285
+ | `synchronization_type` | `synchronizationType` | integer `1/3` | C/U defaults from platform | derived | 1.2.16 sends it on both C and U | C/U | `synchronizationType` |
286
+ | `server_type` | header `X-Server-Type` | integer `0..4` | local Build or fallback `0` | fixed Build table | target namespace, not platform | C/U/D/detail/version | record under same Build |
287
+ | `confirm` | not sent | literal `"DELETE"` | D required | fixed CLI guard | destructive action only | local guard then D | `status=4,isPublic=0`; absent active list |
288
+
289
+ Version history is separate from article metadata:
290
+
291
+ | CLI field | Wire name | JSON type | Required/default | Enum/source | State/linkage rule | Write endpoint | Detail/readback location |
292
+ | --- | --- | --- | --- | --- | --- | --- | --- |
293
+ | `import_id` | `importId` | opaque decimal string | publish required | article detail | parent must exist | `POST /system/user/import/version/publish` | `versionList[].importId`/parent detail |
294
+ | `version` | `version` | non-empty string, max 120 | publish required | caller | expected immutable version label | same | `versionList[].version` |
295
+ | `code_text` | `codeText` | non-empty string | publish required | caller | content hash/length must match evidence | same | current top-level value is not echoed; publish the next version, then read the previous value from `versionList[].codeText` |
296
+ | `changelog` | `changelog` | string, max 10000 | optional; empty omitted | caller | version-specific | same | `versionList[].changelog` |
297
+ | `version_id` | query `versionId` | opaque identifier | delete required | `versionList[].versionId`/`id` | target exact version | `DELETE /system/user/import/version/delete?versionId={id}` | version absent from refreshed list |
298
+ | `confirm` | not sent | literal `"DELETE"` | version delete required | fixed CLI guard | destructive action only | local guard | refreshed `versionList` |
299
+
300
+ ## Image upload and delete contract
301
+
302
+ There are three different image surfaces; they must not be collapsed into one
303
+ generic "image URL" test.
304
+
305
+ | Surface/CLI field | Official 1.2.16 wire | Input/preprocessing | Required headers | Response/reference | Delete behavior | Current CLI status/readback |
306
+ | --- | --- | --- | --- | --- | --- | --- |
307
+ | Creator logo: `logo_base64` / `screenshot_base64s` | C project JSON `screenshotBase64sReqs:{name:"logo.webp",screenshotBase64s:<base64>}` | prepared WebP base64 | Creator JSON auth | project `logo` path | U `imagesOps` upload/delete `logo.webp` | live create, replace, delete, restore and detail readback passed |
308
+ | Creator screenshots | U project JSON `imagesOps` | prepared WebP base64 for upload; names for delete/rename | Creator JSON auth | project `images` count | upload/delete use `name`; rename uses `from`/`to` | two-image upload, rename, single delete and final delete all passed with detail counts |
309
+ | Config cover/gallery: `image_url` | `POST /game/data/file/upload/file/image`, multipart field exactly `file`; one request per image | original local file; up to 10 comma-delimited references | raw `Authorization`, optional `X-Device-Id`; no `X-Server-Type` | key priority `cosStoreKey`, `cosStoreUrl`, `key`, then pathname of `downloadUrl/url/fileUrl`; CSV first key is cover | remove/reorder key in CSV and submit config U; no object-delete endpoint observed | two distinct local images uploaded with byte hashes; returned keys passed config C/U detail readback |
310
+ | WA cover/gallery: `image_url` | same endpoint and multipart `file`; one request per image | original local file; up to 10 comma-delimited references | same; no `X-Server-Type` | same key priority and CSV ordering | remove/reorder key in CSV and submit WA U; no object-delete endpoint observed | two distinct local images uploaded with byte hashes; returned keys passed WA C/U detail readback |
311
+
312
+ The 1.2.16 upload helper is `FormData.append("file", File)` and sets
313
+ `Content-Type: multipart/form-data`; the browser adapter supplies the boundary,
314
+ filename, and part MIME. The CLI multipart helper supplies a generated boundary,
315
+ uses the local basename, derives MIME through `mimetypes.guess_type`, and fixes
316
+ `.webp` to `image/webp`. A key-only response is accepted and its display URL is
317
+ synthesized. The CLI does not reproduce the UI's optional client-side image
318
+ compression; the final implementation's original-file upload was accepted by
319
+ the live service and the returned object keys were used in real C/U requests.
320
+
321
+ ## Build and linked-state regression matrix
322
+
323
+ This is the A10 branch inventory. Positive rows were sent/read back where the
324
+ installed clients expose a writable branch; negative rows fail locally before
325
+ remote mutation.
326
+
327
+ | Branch | Positive cases | Negative case | Current implementation/gate |
328
+ | --- | --- | --- | --- |
329
+ | Build | `0 retail`, `1 classic_era`, `2 classic`, `3 classic_titan`, `4 anniversary`; for each, backup/config/WA list sends identical body `server` and `X-Server-Type` | `<0`, `>4`, non-integer | `_build` and schema reject outside `0..4`; all five live list triplets passed |
330
+ | Creator publish platform | ModUs-only `1`, BigFoot-only `2`, both `3` | empty, unknown, duplicate | state machine covers at-least-one/enum/duplicate |
331
+ | Creator category | 1-5 IDs from live categories | empty, >5, unknown/non-ID; `998` with ModUs/both | local count/linkage plus write preflight membership against `plugin/list/Categories`; real unknown positive ID fails before project mutation |
332
+ | Config/WA platform | omitted defaults ModUs `1`; explicit `1`; explicit ModUs+BigFoot `3` | `0`, `2`, other; mismatched platform/synchronization pairs | schema choices cover numeric invalids; write normalization requires the pair to be present together and equal; it is distinct from Build |
333
+ | Tier mode | normal `subType=0`, season `1`; each allows none by omitting `requiredTierId`; selected tier is positive dynamic ID | zero/negative/non-ID; ID absent from current normal/season options | write preflight reads the selected tier option set and rejects unknown IDs before mutation; the tested account exposes no tier, so real positive writes cover the no-tier branch |
334
+ | Tier + platform | no-tier allows platform `1/3`; selected tier forces platform `1` in UI | selected tier with platform/synchronization `3` | schema rejects tier+BigFoot; current account's live options are empty so real writes use the confirmed no-tier branch |
335
+ | Config WTF | `exclude_wtf=1` with empty account/role; `0` with an account and, when available, one of its roles | `1` with account/role; `0` without account; backup/account/role absent from the selected Build hierarchy; missing role when the account has roles | write preflight reads the selected Build backup hierarchy and rejects every invalid linkage before mutation |
336
+ | Tags | 1, 2, and 3 dynamic IDs | 0, 4, empty/duplicate CSV position, ID absent from the current config/WA tag options | schema covers count/shape/duplicates; write preflight binds every ID to the selected Build's current config/WA options before mutation |
337
+ | Paid/price | installed 1.2.16 forms submit fixed free `is_paid=0,price=0,share_type=0` | paid value, positive/negative price, or nonzero share type | schema/write preflight enforce the installed UI's fixed free branch; real field cycles cover exactly those values and do not claim an unavailable paid branch |
338
+ | Visibility | public `1`, private `0` where UI exposes it | other integer | schema enum covered; WA 1.2.16 form fixes public |
339
+ | Config/WA image CSV | 1 and 10 keys; first key is cover | empty C, >10, empty CSV entries, upload business code >=400 | schema enforces 1-10 non-empty references; media error and live C/U readback covered |
340
+ | Creator image ops | upload, delete, rename with matching required members | missing/extra member, upload without base64, delete with base64 | local validation plus live upload/rename/delete/readback covered |
341
+
342
+ ### Acceptance coverage crosswalk
343
+
344
+ | Acceptance item | Matrix/evidence required here | Documentation status |
345
+ | --- | --- | --- |
346
+ | A2/A3 | Creator project table plus per-field C/U/readback/restore and logo/screenshot/image operations | `analyze/modus-creator/iteration4-live-regression.json`: passed, 140 steps; SHA-256 `7C7BE87959024A1431BC154B13830178815FFF32D23D55E4EAAED75B96B56E4B`; 126 positive exits `0`, 14 expected negative/not-found exits `2`, and unknown positive category membership fails before project mutation; CDN bytes matched local image SHA-256 after create/replace/delete/restore, then final project deletion made the restored logo return 404 |
347
+ | A4 | allocation, metadata, two distinct ZIP PUTs, detail, type/version/path edit/restore and cleanup | same evidence; ZIP SHA-256 and changed MD5/size recorded |
348
+ | A5-A9 | five Builds, config/WA per-field cycles, media upload, version rollover/readback/delete and cleanup | `analyze/modus/live-main-crud-builds-20260826.json`: passed, 209 steps/40 field checks; SHA-256 `298CD33D8E9B72E5AE7B63B6A82627622493CB2DC7476AFE61434E395312BB78`; 191 positive exits `0` and 18 negative exits `2`/`validation_error`; four media uploads carry nonempty byte/SHA-256 evidence for two different binaries, while account/role evidence stores presence only |
349
+ | A10 | linked-state table above, dynamic options and invalid combinations | live option selection plus schema/negative regression passed |
350
+ | A11 | commands, redacted input/response, exit status and content/image/ZIP digest evidence | both evidence records retain hashes instead of credentials or raw content |
351
+ | A19 | complete field names, wire names, types, defaults, sources, dependencies, endpoints and readback locations | matrices in this document |
352
+
353
+ Both main-client modules support explicit `confirm: "DELETE"` on destructive
354
+ commands. List operations send only server fields (`pageNum`, `pageSize`,
355
+ `keyword`, status/filter values), not the local CLI envelope or `server_type`.
356
+ ### Main-client resource IDs and deletion
357
+
358
+ Configuration-share and string/import IDs are opaque decimal strings (for example, 16-digit IDs), not the numeric backup/project IDs. Fupload preserves them as strings. ModUs delete is a soft-delete: a successful `DELETE` returns code `200`, and a subsequent detail may return the record with `status=4` and `isPublic=0`; regression treats that state as deleted.
@@ -1,3 +1,3 @@
1
1
  """Fupload Python CLI."""
2
2
 
3
- __version__ = "0.0.11"
3
+ __version__ = "0.0.15"
@@ -30,11 +30,11 @@ schema exposes them. The calling Skill must show the complete plan and obtain co
30
30
  """
31
31
 
32
32
 
33
- def _modus_provider(*, authenticate: bool = True) -> Any:
33
+ def _modus_provider(*, authenticate: bool = True, main_session: bool = False) -> Any:
34
34
  # Keep ModUs optional at import time so existing platform commands remain
35
35
  # usable while the provider is installed or upgraded independently.
36
36
  from .modus import Modus
37
- return Modus(authenticate=authenticate)
37
+ return Modus(authenticate=authenticate, main_session=main_session)
38
38
 
39
39
 
40
40
  def _parser(**kwargs: Any) -> argparse.ArgumentParser:
@@ -88,6 +88,19 @@ def _read_leaf(parent: argparse._SubParsersAction, name: str, summary: str, **de
88
88
  return leaf
89
89
 
90
90
 
91
+ def _modus_server_flag(parser: argparse.ArgumentParser) -> None:
92
+ """Select the WoW Build sent as ``server`` and ``X-Server-Type``.
93
+
94
+ ``None`` deliberately means "use the main client's current Build". Build
95
+ zero is valid, so this is an integer flag rather than ``_positive``.
96
+ ``--build`` is a concise alias; both names write the same input field.
97
+ """
98
+ parser.add_argument(
99
+ "--server-type", "--build", dest="server_type", type=int, default=None,
100
+ metavar="BUILD_ID", help="WoW Build id; omitted follows the local ModUs current Build (0-4).",
101
+ )
102
+
103
+
91
104
  def _newbee_relationship_tree(parent: argparse.ArgumentParser, resource: str, label: str) -> None:
92
105
  authors = parent.add_parser("co-author", help="Search, list, or replace %s co-authors" % label).add_subparsers(dest="author_action", required=True)
93
106
  leaf = _read_leaf(authors, "search", "Search current co-author candidates.", platform="newbee", resource=resource, action="co-author-search"); leaf.add_argument("--keyword", required=True)
@@ -235,7 +248,14 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
235
248
  )
236
249
  groups = root.add_subparsers(dest="resource_command", required=True)
237
250
  session = groups.add_parser("session", help="Local authentication diagnostics").add_subparsers(dest="action_command", required=True)
238
- _read_leaf(session, "doctor", "Check the local ModUs.Creator token store, DPAPI decryption, and authenticated API readiness without exposing credentials.", platform="modus", resource="session", action="doctor")
251
+ doctor = _read_leaf(session, "doctor", "Check one local ModUs token store, DPAPI decryption, and authenticated API readiness without exposing credentials.", platform="modus", resource="session", action="doctor")
252
+ doctor.add_argument(
253
+ "--client",
254
+ choices=("creator", "main"),
255
+ default="creator",
256
+ help="Authenticated client session to diagnose (technical default: creator).",
257
+ )
258
+ _read_leaf(groups, "builds", "List the fixed ModUs WoW Build ids, codes, names, and the locally selected current Build.", platform="modus", resource="builds", action="list")
239
259
 
240
260
  account = groups.add_parser("account", help="ModUs author account and statistics").add_subparsers(dest="action_command", required=True)
241
261
  _read_leaf(account, "info", "Read the current ModUs account capability flags.", platform="modus", resource="account", action="info")
@@ -245,13 +265,13 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
245
265
  addon = groups.add_parser("addon", help="ModUs addon discovery and history APIs").add_subparsers(dest="action_command", required=True)
246
266
  leaf = _read_leaf(addon, "info", "Resolve addon directories to project records.", platform="modus", resource="addon", action="info")
247
267
  leaf.add_argument("--directory", dest="directories", action="append", required=True)
248
- leaf.add_argument("--server-type", type=int, default=1)
268
+ _modus_server_flag(leaf)
249
269
  leaf = _read_leaf(addon, "project-info", "Resolve addon project IDs to names.", platform="modus", resource="addon", action="project-info")
250
270
  leaf.add_argument("--project-id", dest="project_ids", action="append", type=_positive, required=True)
251
- leaf.add_argument("--server-type", type=int, default=1)
271
+ _modus_server_flag(leaf)
252
272
  leaf = _read_leaf(addon, "history", "Read addon project version history.", platform="modus", resource="addon", action="history")
253
273
  leaf.add_argument("--project-id", type=_positive, required=True)
254
- leaf.add_argument("--server-type", type=int, default=1)
274
+ _modus_server_flag(leaf)
255
275
  _page_flags(leaf)
256
276
 
257
277
  options = groups.add_parser("options", help="Read ModUs dynamic choices").add_subparsers(dest="option_action", required=True)
@@ -259,6 +279,9 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
259
279
  game_versions = _read_leaf(options, "game-versions", "List supported ModUs game versions.", platform="modus", resource="options", action="game-versions")
260
280
  game_versions.add_argument("--key", dest="keys", action="append", required=True, help="Request one game config key; repeatable.")
261
281
  _read_leaf(options, "subscription-tiers", "List author subscription tiers returned by ModUs.", platform="modus", resource="options", action="subscription-tiers")
282
+ _read_leaf(options, "config-tags", "List configuration-share tag choices from the official ModUs client data.", platform="modus", resource="options", action="config-tags")
283
+ _read_leaf(options, "wa-tags", "List string tag choices from the official ModUs client data.", platform="modus", resource="options", action="wa-tags")
284
+ _read_leaf(options, "wa-support-addons", "List applicable-addon choices from the official ModUs client data.", platform="modus", resource="options", action="wa-support-addons")
262
285
 
263
286
  project = groups.add_parser("project", help="ModUs plugin project records").add_subparsers(dest="action_command", required=True)
264
287
  for action, text in (
@@ -282,9 +305,26 @@ def _modus_tree(platforms: argparse._SubParsersAction) -> None:
282
305
  ("delete", "Delete one explicitly confirmed ModUs plugin release."),
283
306
  ):
284
307
  _write_leaf(plugin, "modus", "plugin", action, text)
285
- leaf = _read_leaf(plugin, "list", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="list"); leaf.add_argument("--project-id", type=_positive, required=True); _page_flags(leaf)
286
- leaf = _read_leaf(plugin, "get", "Read one ModUs plugin release detail.", platform="modus", resource="plugin", action="get"); leaf.add_argument("--project-id", type=_positive, required=True); leaf.add_argument("--file-id", type=_positive, required=True)
287
- leaf = _read_leaf(plugin, "versions", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="versions"); leaf.add_argument("--project-id", type=_positive, required=True); _page_flags(leaf)
308
+ leaf = _read_leaf(plugin, "list", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="list"); leaf.add_argument("--project-id", type=_positive, required=True); _modus_server_flag(leaf); _page_flags(leaf)
309
+ leaf = _read_leaf(plugin, "get", "Read one ModUs plugin release detail.", platform="modus", resource="plugin", action="get"); leaf.add_argument("--project-id", type=_positive, required=True); leaf.add_argument("--file-id", type=_positive, required=True); _modus_server_flag(leaf)
310
+ leaf = _read_leaf(plugin, "versions", "List releases for one ModUs plugin project.", platform="modus", resource="plugin", action="versions"); leaf.add_argument("--project-id", type=_positive, required=True); _modus_server_flag(leaf); _page_flags(leaf)
311
+
312
+ media = groups.add_parser("media", help="ModUs main-client image uploads").add_subparsers(dest="action_command", required=True)
313
+ _write_leaf(media, "modus", "media", "upload", "Upload one local image and return its reusable ModUs object key and URL.")
314
+
315
+ config = groups.add_parser("config", help="ModUs main-client configuration shares and backups").add_subparsers(dest="action_command", required=True)
316
+ for action, text in (("create", "Create a configuration share from an existing ModUs cloud backup."), ("update", "Update a configuration share."), ("edit", "Edit a configuration share."), ("delete", "Delete one explicitly confirmed configuration share."), ("backup-edit", "Rename an existing ModUs cloud backup."), ("backup-delete", "Delete one explicitly confirmed ModUs cloud backup.")):
317
+ _write_leaf(config, "modus", "config", action, text)
318
+ leaf = _read_leaf(config, "list", "List ModUs configuration shares.", platform="modus", resource="config", action="list"); _modus_server_flag(leaf); leaf.add_argument("--page-num", type=int); leaf.add_argument("--page-size", type=int); leaf.add_argument("--keyword"); leaf.add_argument("--status", type=int); leaf.add_argument("--share-type", type=int); leaf.add_argument("--order-by"); leaf.add_argument("--mine", action=argparse.BooleanOptionalAction); leaf.add_argument("--is-public", type=int, choices=(0, 1)); leaf.add_argument("--is-paid", type=int, choices=(0, 1)); leaf.add_argument("--tags")
319
+ leaf = _read_leaf(config, "get", "Read a ModUs configuration share detail.", platform="modus", resource="config", action="get"); leaf.add_argument("--share-id", required=True); _modus_server_flag(leaf)
320
+ leaf = _read_leaf(config, "backups", "List existing ModUs cloud backups.", platform="modus", resource="config", action="backups"); _modus_server_flag(leaf)
321
+ leaf = _read_leaf(config, "backup-get", "Read one ModUs cloud backup detail.", platform="modus", resource="config", action="backup-get"); leaf.add_argument("--backup-id", type=_positive, required=True); _modus_server_flag(leaf)
322
+
323
+ wa = groups.add_parser("wa", help="ModUs main-client string articles").add_subparsers(dest="action_command", required=True)
324
+ for action, text in (("create", "Create a ModUs string article."), ("update", "Update a ModUs string article."), ("edit", "Edit a ModUs string article."), ("delete", "Delete one explicitly confirmed string article."), ("version-publish", "Publish a new string article version."), ("version-delete", "Delete one explicitly confirmed historical string version.")):
325
+ _write_leaf(wa, "modus", "wa", action, text)
326
+ leaf = _read_leaf(wa, "list", "List ModUs string articles.", platform="modus", resource="wa", action="list"); _modus_server_flag(leaf); leaf.add_argument("--page-num", type=int); leaf.add_argument("--page-size", type=int); leaf.add_argument("--keyword"); leaf.add_argument("--status", type=int); leaf.add_argument("--order-by"); leaf.add_argument("--mine", action=argparse.BooleanOptionalAction); leaf.add_argument("--support-addon"); leaf.add_argument("--tags"); leaf.add_argument("--is-paid", type=int, choices=(0, 1))
327
+ leaf = _read_leaf(wa, "get", "Read one ModUs string article detail.", platform="modus", resource="wa", action="get"); leaf.add_argument("--import-id", required=True); _modus_server_flag(leaf)
288
328
 
289
329
 
290
330
  def build_parser() -> argparse.ArgumentParser:
@@ -347,7 +387,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
347
387
  if args.dry_run:
348
388
  write_output(platform, operation, _dry_run_data(doc, schema.name), dry_run=True)
349
389
  return 0
350
- provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider())))
390
+ provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(main_session=resource in ("media", "config", "wa")))))
351
391
  try:
352
392
  if platform == "dd":
353
393
  data = provider.execute_write(resource, action, doc, getattr(args, "session", None))
@@ -360,10 +400,18 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
360
400
  write_output(platform, operation, data)
361
401
  return 0
362
402
  modus_doctor = platform == "modus" and resource == "session" and action == "doctor"
363
- provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(authenticate=not modus_doctor))))
403
+ modus_main_session = resource in ("addon", "plugin", "config", "wa", "builds") or (
404
+ modus_doctor and getattr(args, "client", "creator") == "main"
405
+ )
406
+ provider = NewBee() if platform == "newbee" else (DD() if platform == "dd" else (Blackbox() if platform == "blackbox" else (CurseForge() if platform == "curseforge" else _modus_provider(authenticate=not modus_doctor, main_session=modus_main_session))))
364
407
  try:
365
408
  if platform == "dd":
366
409
  data = provider.execute_read(resource, action, args, getattr(args, "session", None))
410
+ elif platform == "modus" and resource == "builds" and action == "list":
411
+ # Build discovery is local and deterministic; it must not
412
+ # accidentally hit a platform endpoint or confuse platform
413
+ # selection with the Build id sent on CRUD requests.
414
+ data = provider.builds()
367
415
  else:
368
416
  data = provider.execute_read(resource, action, args)
369
417
  finally: