@follenfang/fupload 0.0.8 → 0.0.10
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 +1 -1
- package/fupload/SKILL.md +17 -4
- package/fupload/references/modus.md +77 -0
- package/fupload/scripts/fupload_cli/__init__.py +1 -1
- package/fupload/scripts/fupload_cli/cli.py +70 -2
- package/fupload/scripts/fupload_cli/io.py +3 -1
- package/fupload/scripts/fupload_cli/modus.py +768 -0
- package/fupload/scripts/fupload_cli/modus_zip.py +175 -0
- package/fupload/scripts/fupload_cli/schema.py +141 -2
- package/fupload/scripts/fupload_cli/state_machine.py +346 -0
- package/npm/lib/python.mjs +56 -7
- package/npm/skill-manifest.json +33 -13
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""ModUs Creator project form state machine.
|
|
2
|
+
|
|
3
|
+
The desktop Creator exposes three form tabs in order: choose a game, enter
|
|
4
|
+
general project information, and select/build a license. This module keeps
|
|
5
|
+
that sequencing explicit for CLI callers and provides a small JSON-safe
|
|
6
|
+
snapshot that can be resumed after an interrupted form submission.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, Mapping, Optional, Union
|
|
18
|
+
|
|
19
|
+
from .errors import ValidationError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectStep(str, Enum):
|
|
23
|
+
"""Persisted steps exposed by the Creator form."""
|
|
24
|
+
|
|
25
|
+
CHOOSE_GAME = "choose_game"
|
|
26
|
+
SELECT_GAME = "choose_game"
|
|
27
|
+
BASIC_INFO = "basic_info"
|
|
28
|
+
GENERAL = "basic_info"
|
|
29
|
+
LICENSE = "license"
|
|
30
|
+
COMPLETE = "complete"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# String aliases make the state machine convenient for JSON/CLI consumers.
|
|
34
|
+
CHOOSE_GAME = ProjectStep.CHOOSE_GAME.value
|
|
35
|
+
SELECT_GAME = CHOOSE_GAME
|
|
36
|
+
BASIC_INFO = ProjectStep.BASIC_INFO.value
|
|
37
|
+
GENERAL = BASIC_INFO
|
|
38
|
+
LICENSE = ProjectStep.LICENSE.value
|
|
39
|
+
COMPLETE = ProjectStep.COMPLETE.value
|
|
40
|
+
|
|
41
|
+
_SCHEMA = "fupload.v1.modus.project-state"
|
|
42
|
+
_PLATFORMS = ("modus", "bigfoot")
|
|
43
|
+
_BIGFOOT_EXCLUSIVE_CATEGORY_ID = 998
|
|
44
|
+
_LICENSE_KEYS = {
|
|
45
|
+
"type", "template", "license_template", "licenseTemplate",
|
|
46
|
+
"holder", "copyright_holder", "copyrightHolder",
|
|
47
|
+
"year", "copyright_year", "copyrightYear",
|
|
48
|
+
"content", "license_content", "licenseContent",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _nonempty_text(value: Any, *, path: str) -> str:
|
|
53
|
+
if not isinstance(value, str) or not value.strip():
|
|
54
|
+
raise ValidationError("value must be a non-empty string", path=path)
|
|
55
|
+
return value.strip()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _required_tier(value: Any, *, path: str = "$.required_tier_id") -> Optional[int]:
|
|
59
|
+
"""Validate Creator's optional subscription-tier selection.
|
|
60
|
+
|
|
61
|
+
``None`` is the explicit "no required tier" branch. The API contract
|
|
62
|
+
uses a positive integer for a selected tier; booleans and numeric strings
|
|
63
|
+
are rejected to avoid silently selecting the wrong dropdown item.
|
|
64
|
+
"""
|
|
65
|
+
if value is None:
|
|
66
|
+
return None
|
|
67
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
68
|
+
raise ValidationError("required_tier_id must be null or a positive integer", path=path)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _platforms(value: Any, *, path: str = "$.publish_platforms") -> list[str]:
|
|
73
|
+
if not isinstance(value, (list, tuple)) or not value:
|
|
74
|
+
raise ValidationError("publish_platforms must contain at least one platform", path=path)
|
|
75
|
+
result: list[str] = []
|
|
76
|
+
for index, item in enumerate(value):
|
|
77
|
+
if not isinstance(item, str) or item not in _PLATFORMS:
|
|
78
|
+
raise ValidationError("platform must be modus or bigfoot", path=f"{path}[{index}]")
|
|
79
|
+
if item in result:
|
|
80
|
+
raise ValidationError("publish_platforms must not contain duplicates", path=path)
|
|
81
|
+
result.append(item)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _categories(value: Any, *, path: str = "$.categories") -> list[int]:
|
|
86
|
+
if not isinstance(value, list) or not value:
|
|
87
|
+
raise ValidationError("categories must contain at least one category ID", path=path)
|
|
88
|
+
if len(value) > 5:
|
|
89
|
+
raise ValidationError("categories must contain at most five category IDs", path=path)
|
|
90
|
+
result: list[int] = []
|
|
91
|
+
for index, item in enumerate(value):
|
|
92
|
+
if isinstance(item, bool) or not isinstance(item, int) or item <= 0:
|
|
93
|
+
raise ValidationError("category ID must be a positive integer", path=f"{path}[{index}]")
|
|
94
|
+
if item in result:
|
|
95
|
+
raise ValidationError("categories must not contain duplicates", path=path)
|
|
96
|
+
result.append(item)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _game(value: Any) -> Any:
|
|
101
|
+
"""Require a game object/value that can be serialized and read back."""
|
|
102
|
+
if isinstance(value, Mapping):
|
|
103
|
+
if not value:
|
|
104
|
+
raise ValidationError("game selection must not be empty", path="$.game")
|
|
105
|
+
# Creator game entries have an ID/key and a display name. Preserve
|
|
106
|
+
# unknown fields for forward compatibility, but require one stable
|
|
107
|
+
# identity field so a resumed snapshot remains addressable.
|
|
108
|
+
identity = ("id", "gameId", "game_id", "key", "gameVersion", "game_version", "server")
|
|
109
|
+
if not any(value.get(name) not in (None, "") for name in identity):
|
|
110
|
+
raise ValidationError("game selection must include an identifier", path="$.game")
|
|
111
|
+
return copy.deepcopy(dict(value))
|
|
112
|
+
if isinstance(value, str) and value.strip():
|
|
113
|
+
return value.strip()
|
|
114
|
+
raise ValidationError("game selection must be a non-empty object or string", path="$.game")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _license(value: Any) -> Dict[str, Any]:
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
value = {"type": value}
|
|
120
|
+
if not isinstance(value, Mapping):
|
|
121
|
+
raise ValidationError("license must be a non-empty object or string", path="$.license")
|
|
122
|
+
unknown = sorted(set(value) - _LICENSE_KEYS)
|
|
123
|
+
if unknown:
|
|
124
|
+
raise ValidationError("unknown license field(s): %s" % ", ".join(unknown), path="$.license.%s" % unknown[0])
|
|
125
|
+
aliases = {
|
|
126
|
+
"type": ("type", "template", "license_template", "licenseTemplate"),
|
|
127
|
+
"holder": ("holder", "copyright_holder", "copyrightHolder"),
|
|
128
|
+
"year": ("year", "copyright_year", "copyrightYear"),
|
|
129
|
+
"content": ("content", "license_content", "licenseContent"),
|
|
130
|
+
}
|
|
131
|
+
result: Dict[str, Any] = {}
|
|
132
|
+
for target, names in aliases.items():
|
|
133
|
+
for name in names:
|
|
134
|
+
if name in value and value[name] is not None:
|
|
135
|
+
result[target] = value[name]
|
|
136
|
+
break
|
|
137
|
+
result["type"] = _nonempty_text(result.get("type"), path="$.license.type")
|
|
138
|
+
for name in ("holder", "year", "content"):
|
|
139
|
+
if name in result and result[name] is not None:
|
|
140
|
+
result[name] = _nonempty_text(result[name], path="$.license.%s" % name)
|
|
141
|
+
if result["type"].lower() in {"custom", "自定义"} and not result.get("content"):
|
|
142
|
+
raise ValidationError("custom license content must not be empty", path="$.license.content")
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ProjectStateMachine:
|
|
147
|
+
"""Sequenced, resumable ModUs project creation/edit form."""
|
|
148
|
+
|
|
149
|
+
schema = _SCHEMA
|
|
150
|
+
|
|
151
|
+
def __init__(self, snapshot: Optional[Mapping[str, Any]] = None) -> None:
|
|
152
|
+
self._step = ProjectStep.CHOOSE_GAME.value
|
|
153
|
+
self._game: Any = None
|
|
154
|
+
self._basic_info: Dict[str, Any] = {}
|
|
155
|
+
self._license: Dict[str, Any] = {}
|
|
156
|
+
if snapshot is not None:
|
|
157
|
+
self._restore(snapshot)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def state(self) -> str:
|
|
161
|
+
return self._step
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def current_step(self) -> str:
|
|
165
|
+
return self._step
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def game(self) -> Any:
|
|
169
|
+
return copy.deepcopy(self._game)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def basic_info(self) -> Dict[str, Any]:
|
|
173
|
+
return copy.deepcopy(self._basic_info)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def license(self) -> Dict[str, Any]:
|
|
177
|
+
return copy.deepcopy(self._license)
|
|
178
|
+
|
|
179
|
+
def _require(self, expected: ProjectStep) -> None:
|
|
180
|
+
if self._step != expected.value:
|
|
181
|
+
raise ValidationError(
|
|
182
|
+
"project state %s cannot submit step %s"
|
|
183
|
+
% (self._step, expected.value),
|
|
184
|
+
path="$.state",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def select_game(self, game: Any) -> Dict[str, Any]:
|
|
188
|
+
self._require(ProjectStep.CHOOSE_GAME)
|
|
189
|
+
self._game = _game(game)
|
|
190
|
+
self._step = ProjectStep.BASIC_INFO.value
|
|
191
|
+
return self.snapshot()
|
|
192
|
+
|
|
193
|
+
def submit_basic_info(self, info: Optional[Mapping[str, Any]] = None, **fields: Any) -> Dict[str, Any]:
|
|
194
|
+
self._require(ProjectStep.BASIC_INFO)
|
|
195
|
+
value: Dict[str, Any] = dict(info or {})
|
|
196
|
+
value.update(fields)
|
|
197
|
+
name = value.get("name", value.get("project_name"))
|
|
198
|
+
summary = value.get("summary")
|
|
199
|
+
_nonempty_text(name, path="$.basic_info.name")
|
|
200
|
+
_nonempty_text(summary, path="$.basic_info.summary")
|
|
201
|
+
selected = value.get("publish_platforms", value.get("publishPlatforms"))
|
|
202
|
+
value["publish_platforms"] = _platforms(selected, path="$.basic_info.publish_platforms")
|
|
203
|
+
value.pop("publishPlatforms", None)
|
|
204
|
+
value["categories"] = _categories(value.get("categories"), path="$.basic_info.categories")
|
|
205
|
+
if _BIGFOOT_EXCLUSIVE_CATEGORY_ID in value["categories"] and value["publish_platforms"] != ["bigfoot"]:
|
|
206
|
+
raise ValidationError(
|
|
207
|
+
"category 998 requires bigfoot as the only publish platform",
|
|
208
|
+
path="$.basic_info.publish_platforms",
|
|
209
|
+
)
|
|
210
|
+
if "required_tier_id" in value:
|
|
211
|
+
value["required_tier_id"] = _required_tier(value["required_tier_id"], path="$.basic_info.required_tier_id")
|
|
212
|
+
elif "requiredTierId" in value:
|
|
213
|
+
value["required_tier_id"] = _required_tier(value.pop("requiredTierId"), path="$.basic_info.requiredTierId")
|
|
214
|
+
else:
|
|
215
|
+
# Explicitly persist the no-tier branch so reloads are stable.
|
|
216
|
+
value["required_tier_id"] = None
|
|
217
|
+
if "bigfoot" in value["publish_platforms"] and value["required_tier_id"] is not None:
|
|
218
|
+
raise ValidationError(
|
|
219
|
+
"required_tier_id must be null when bigfoot is selected",
|
|
220
|
+
path="$.basic_info.required_tier_id",
|
|
221
|
+
)
|
|
222
|
+
derived_sync_type = (1 if "modus" in value["publish_platforms"] else 0) | (
|
|
223
|
+
2 if "bigfoot" in value["publish_platforms"] else 0
|
|
224
|
+
)
|
|
225
|
+
# Creator exposes platform toggles, not synchronizationType itself.
|
|
226
|
+
# Always replace a stale caller value with the UI-derived bit mask.
|
|
227
|
+
value["synchronization_type"] = derived_sync_type
|
|
228
|
+
if "project_name" in value:
|
|
229
|
+
value["name"] = value.pop("project_name")
|
|
230
|
+
self._basic_info = copy.deepcopy(value)
|
|
231
|
+
self._step = ProjectStep.LICENSE.value
|
|
232
|
+
return self.snapshot()
|
|
233
|
+
|
|
234
|
+
def submit_license(self, license_value: Any = None, **fields: Any) -> Dict[str, Any]:
|
|
235
|
+
self._require(ProjectStep.LICENSE)
|
|
236
|
+
value: Any = license_value
|
|
237
|
+
if value is None and fields:
|
|
238
|
+
value = fields
|
|
239
|
+
elif fields:
|
|
240
|
+
if not isinstance(value, Mapping):
|
|
241
|
+
raise ValidationError("license fields require an object", path="$.license")
|
|
242
|
+
merged = dict(value)
|
|
243
|
+
merged.update(fields)
|
|
244
|
+
value = merged
|
|
245
|
+
self._license = _license(value)
|
|
246
|
+
self._step = ProjectStep.COMPLETE.value
|
|
247
|
+
return self.snapshot()
|
|
248
|
+
|
|
249
|
+
# Friendly aliases used by form adapters that name the first tab
|
|
250
|
+
# "select game" and the second tab "general".
|
|
251
|
+
choose_game = select_game
|
|
252
|
+
set_game = select_game
|
|
253
|
+
set_basic_info = submit_basic_info
|
|
254
|
+
set_license = submit_license
|
|
255
|
+
|
|
256
|
+
def submit(self, step: str, payload: Any = None, **fields: Any) -> Dict[str, Any]:
|
|
257
|
+
"""Dispatch a named step for generic CLI adapters."""
|
|
258
|
+
normalized = str(step).strip().lower().replace("-", "_")
|
|
259
|
+
if normalized in {"choose_game", "game"}:
|
|
260
|
+
return self.select_game(payload)
|
|
261
|
+
if normalized in {"basic_info", "general", "basic"}:
|
|
262
|
+
return self.submit_basic_info(payload, **fields)
|
|
263
|
+
if normalized == "license":
|
|
264
|
+
return self.submit_license(payload, **fields)
|
|
265
|
+
raise ValidationError("unsupported project state step", path="$.state")
|
|
266
|
+
|
|
267
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
268
|
+
return {
|
|
269
|
+
"schema": self.schema,
|
|
270
|
+
"state": self._step,
|
|
271
|
+
"game": copy.deepcopy(self._game),
|
|
272
|
+
"basic_info": copy.deepcopy(self._basic_info),
|
|
273
|
+
"license": copy.deepcopy(self._license),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
to_dict = snapshot
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_snapshot(cls, snapshot: Mapping[str, Any]) -> "ProjectStateMachine":
|
|
280
|
+
return cls(snapshot)
|
|
281
|
+
|
|
282
|
+
def _restore(self, snapshot: Mapping[str, Any]) -> None:
|
|
283
|
+
if not isinstance(snapshot, Mapping) or snapshot.get("schema") != self.schema:
|
|
284
|
+
raise ValidationError("invalid project state snapshot", path="$.schema")
|
|
285
|
+
state = snapshot.get("state")
|
|
286
|
+
if state not in {item.value for item in ProjectStep}:
|
|
287
|
+
raise ValidationError("invalid project state", path="$.state")
|
|
288
|
+
game = snapshot.get("game")
|
|
289
|
+
basic = snapshot.get("basic_info") or {}
|
|
290
|
+
lic = snapshot.get("license") or {}
|
|
291
|
+
if state != CHOOSE_GAME:
|
|
292
|
+
self._game = _game(game)
|
|
293
|
+
if state in {LICENSE, COMPLETE}:
|
|
294
|
+
if not isinstance(basic, Mapping):
|
|
295
|
+
raise ValidationError("basic_info must be an object", path="$.basic_info")
|
|
296
|
+
self._basic_info = dict(basic)
|
|
297
|
+
_nonempty_text(self._basic_info.get("name"), path="$.basic_info.name")
|
|
298
|
+
_nonempty_text(self._basic_info.get("summary"), path="$.basic_info.summary")
|
|
299
|
+
self._basic_info["publish_platforms"] = _platforms(self._basic_info.get("publish_platforms"))
|
|
300
|
+
self._basic_info["categories"] = _categories(self._basic_info.get("categories"))
|
|
301
|
+
self._basic_info["required_tier_id"] = _required_tier(self._basic_info.get("required_tier_id"))
|
|
302
|
+
if _BIGFOOT_EXCLUSIVE_CATEGORY_ID in self._basic_info["categories"] and self._basic_info["publish_platforms"] != ["bigfoot"]:
|
|
303
|
+
raise ValidationError("category 998 requires bigfoot as the only publish platform", path="$.basic_info.publish_platforms")
|
|
304
|
+
if "bigfoot" in self._basic_info["publish_platforms"] and self._basic_info["required_tier_id"] is not None:
|
|
305
|
+
raise ValidationError("required_tier_id must be null when bigfoot is selected", path="$.basic_info.required_tier_id")
|
|
306
|
+
derived_sync_type = (1 if "modus" in self._basic_info["publish_platforms"] else 0) | (2 if "bigfoot" in self._basic_info["publish_platforms"] else 0)
|
|
307
|
+
if self._basic_info.get("synchronization_type") != derived_sync_type:
|
|
308
|
+
raise ValidationError("synchronization_type must match publish_platforms", path="$.basic_info.synchronization_type")
|
|
309
|
+
if state == COMPLETE:
|
|
310
|
+
self._license = _license(lic)
|
|
311
|
+
self._step = str(state)
|
|
312
|
+
|
|
313
|
+
def save(self, path: Union[str, os.PathLike[str]]) -> Path:
|
|
314
|
+
target = Path(path)
|
|
315
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
316
|
+
fd, temporary = tempfile.mkstemp(prefix=target.name + ".", suffix=".tmp", dir=str(target.parent))
|
|
317
|
+
try:
|
|
318
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
319
|
+
json.dump(self.snapshot(), handle, ensure_ascii=False, indent=2)
|
|
320
|
+
handle.write("\n")
|
|
321
|
+
handle.flush()
|
|
322
|
+
os.fsync(handle.fileno())
|
|
323
|
+
os.replace(temporary, target)
|
|
324
|
+
finally:
|
|
325
|
+
if os.path.exists(temporary):
|
|
326
|
+
os.unlink(temporary)
|
|
327
|
+
return target
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def load(cls, path: Union[str, os.PathLike[str]]) -> "ProjectStateMachine":
|
|
331
|
+
target = Path(path)
|
|
332
|
+
try:
|
|
333
|
+
with target.open("r", encoding="utf-8") as handle:
|
|
334
|
+
snapshot = json.load(handle)
|
|
335
|
+
except (OSError, ValueError) as exc:
|
|
336
|
+
raise ValidationError("project state snapshot could not be read", path=str(target)) from exc
|
|
337
|
+
return cls.from_snapshot(snapshot)
|
|
338
|
+
|
|
339
|
+
save_state = save
|
|
340
|
+
load_state = load
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = [
|
|
344
|
+
"ProjectStep", "ProjectStateMachine", "CHOOSE_GAME", "SELECT_GAME",
|
|
345
|
+
"BASIC_INFO", "GENERAL", "LICENSE", "COMPLETE",
|
|
346
|
+
]
|
package/npm/lib/python.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
|
|
7
7
|
import { productStateRoot } from "./managed-install.mjs";
|
|
8
8
|
|
|
9
|
-
export const PYTHON_RUNTIME_SCHEMA = "fupload.python-runtime.
|
|
9
|
+
export const PYTHON_RUNTIME_SCHEMA = "fupload.python-runtime.v2";
|
|
10
10
|
const RUNTIME_DIRECTORY = "python";
|
|
11
11
|
const RUNTIME_MARKER = "runtime.json";
|
|
12
12
|
const RUNTIME_LOCK_WAIT_MS = 5 * 60 * 1000;
|
|
@@ -45,6 +45,22 @@ export function discoverPython({ platform = process.platform, minimumMinor = 9 }
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function discoverUv({ run = spawnSync } = {}) {
|
|
49
|
+
const result = run("uv", ["--version"], {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
shell: false,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
if (result.error || result.status !== 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const match = result.stdout.trim().match(/^uv (\d+)\.(\d+)\.(\d+)(?:\s|$)/);
|
|
58
|
+
if (!match) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { command: "uv", args: [], version: match.slice(1).map(Number) };
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
export function runPython(python, script, args, options = {}) {
|
|
49
65
|
const run = options.run || spawnSync;
|
|
50
66
|
const env = options.runtimeRoot
|
|
@@ -129,7 +145,10 @@ function probeRuntime(executable, root, env, run = spawnSync) {
|
|
|
129
145
|
|
|
130
146
|
function inspectRuntime({ root, platform, requirementsHash, env, run }) {
|
|
131
147
|
const marker = readMarker(root);
|
|
132
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
marker?.schema !== PYTHON_RUNTIME_SCHEMA ||
|
|
150
|
+
marker.requirements_sha256 !== requirementsHash
|
|
151
|
+
) {
|
|
133
152
|
return null;
|
|
134
153
|
}
|
|
135
154
|
const command = pythonRuntimeExecutable(root, platform);
|
|
@@ -210,6 +229,7 @@ export function ensurePythonRuntime({
|
|
|
210
229
|
home = os.homedir(),
|
|
211
230
|
run = spawnSync,
|
|
212
231
|
discover = discoverPython,
|
|
232
|
+
discoverUv: locateUv = discoverUv,
|
|
213
233
|
} = {}) {
|
|
214
234
|
const requirements = pythonRequirementsFile(packageRoot);
|
|
215
235
|
const content = fs.readFileSync(requirements);
|
|
@@ -218,7 +238,15 @@ export function ensurePythonRuntime({
|
|
|
218
238
|
const parent = path.dirname(root);
|
|
219
239
|
const lock = acquireRuntimeLock(root);
|
|
220
240
|
try {
|
|
221
|
-
const
|
|
241
|
+
const uv = locateUv({ run });
|
|
242
|
+
const dependencyInstaller = uv ? "uv" : "pip";
|
|
243
|
+
const current = inspectRuntime({
|
|
244
|
+
root,
|
|
245
|
+
platform,
|
|
246
|
+
requirementsHash,
|
|
247
|
+
env,
|
|
248
|
+
run,
|
|
249
|
+
});
|
|
222
250
|
if (current) {
|
|
223
251
|
return { status: "current", root, requirements, python: current };
|
|
224
252
|
}
|
|
@@ -236,9 +264,22 @@ export function ensurePythonRuntime({
|
|
|
236
264
|
try {
|
|
237
265
|
runChecked(run, base.command, [...base.args, "-m", "venv", staging], "Could not create the Fuploader Python runtime");
|
|
238
266
|
const stagingPython = pythonRuntimeExecutable(staging, platform);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
267
|
+
if (uv) {
|
|
268
|
+
runChecked(run, uv.command, [
|
|
269
|
+
...uv.args,
|
|
270
|
+
"pip", "install",
|
|
271
|
+
"--python", stagingPython,
|
|
272
|
+
"--requirements", requirements,
|
|
273
|
+
"--link-mode", "copy",
|
|
274
|
+
"--no-config",
|
|
275
|
+
"--no-progress",
|
|
276
|
+
"--no-python-downloads",
|
|
277
|
+
], "Could not install Fuploader Python dependencies with uv");
|
|
278
|
+
} else {
|
|
279
|
+
runChecked(run, stagingPython, [
|
|
280
|
+
"-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--requirement", requirements,
|
|
281
|
+
], "Could not install Fuploader Python dependencies");
|
|
282
|
+
}
|
|
242
283
|
runChecked(run, stagingPython, [
|
|
243
284
|
"-m", "playwright", "install", "chromium",
|
|
244
285
|
], "Could not install Fuploader Chromium", {
|
|
@@ -251,6 +292,8 @@ export function ensurePythonRuntime({
|
|
|
251
292
|
writeMarker(staging, {
|
|
252
293
|
schema: PYTHON_RUNTIME_SCHEMA,
|
|
253
294
|
requirements_sha256: requirementsHash,
|
|
295
|
+
dependency_installer: dependencyInstaller,
|
|
296
|
+
dependency_installer_version: uv ? uv.version.join(".") : null,
|
|
254
297
|
python_version: installed.version.join("."),
|
|
255
298
|
dependency_version: installed.dependencyVersion,
|
|
256
299
|
playwright_version: installed.playwrightVersion,
|
|
@@ -269,7 +312,13 @@ export function ensurePythonRuntime({
|
|
|
269
312
|
}
|
|
270
313
|
throw error;
|
|
271
314
|
}
|
|
272
|
-
created = inspectRuntime({
|
|
315
|
+
created = inspectRuntime({
|
|
316
|
+
root,
|
|
317
|
+
platform,
|
|
318
|
+
requirementsHash,
|
|
319
|
+
env,
|
|
320
|
+
run,
|
|
321
|
+
});
|
|
273
322
|
if (!created) {
|
|
274
323
|
fs.rmSync(root, { recursive: true, force: true });
|
|
275
324
|
if (movedOld) {
|
package/npm/skill-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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.10",
|
|
5
|
+
"skill_version": "0.0.10",
|
|
6
|
+
"tree_sha256": "38621aa4709de68a1cc298c8183fe0a5647c8291b870defdbbb5f54d184fa94a",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": "agents/openai.yaml",
|
|
@@ -90,6 +90,11 @@
|
|
|
90
90
|
"bytes": 16543,
|
|
91
91
|
"sha256": "782d7c89d865df545fb6ffe4d7ee4492e6e5b192a3f71e4aef6424183a6a4f24"
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
"path": "references/modus.md",
|
|
95
|
+
"bytes": 4492,
|
|
96
|
+
"sha256": "0b9cf6c4b3bc7aa37a3649ec6ccb6c8b8aa3d077c8febc2635548546150df356"
|
|
97
|
+
},
|
|
93
98
|
{
|
|
94
99
|
"path": "references/newbee-official-cli.md",
|
|
95
100
|
"bytes": 24919,
|
|
@@ -107,8 +112,8 @@
|
|
|
107
112
|
},
|
|
108
113
|
{
|
|
109
114
|
"path": "scripts/fupload_cli/__init__.py",
|
|
110
|
-
"bytes":
|
|
111
|
-
"sha256": "
|
|
115
|
+
"bytes": 50,
|
|
116
|
+
"sha256": "3f1afb95137eaedc622bb437c9c8a01bdaa729b911f3749e400a429d24abaaa2"
|
|
112
117
|
},
|
|
113
118
|
{
|
|
114
119
|
"path": "scripts/fupload_cli/blackbox_web.py",
|
|
@@ -122,8 +127,8 @@
|
|
|
122
127
|
},
|
|
123
128
|
{
|
|
124
129
|
"path": "scripts/fupload_cli/cli.py",
|
|
125
|
-
"bytes":
|
|
126
|
-
"sha256": "
|
|
130
|
+
"bytes": 31958,
|
|
131
|
+
"sha256": "897c2e7a7fea00125023d7fc977aaa6e3b7729b50a468e7deadfacc04d89f9b8"
|
|
127
132
|
},
|
|
128
133
|
{
|
|
129
134
|
"path": "scripts/fupload_cli/curseforge.py",
|
|
@@ -152,8 +157,18 @@
|
|
|
152
157
|
},
|
|
153
158
|
{
|
|
154
159
|
"path": "scripts/fupload_cli/io.py",
|
|
155
|
-
"bytes":
|
|
156
|
-
"sha256": "
|
|
160
|
+
"bytes": 4515,
|
|
161
|
+
"sha256": "dafcfcaa493def7bc42719f357416208a3cabbb163cf651fb18307b501b46e4b"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"path": "scripts/fupload_cli/modus_zip.py",
|
|
165
|
+
"bytes": 7833,
|
|
166
|
+
"sha256": "3b35b30266dd48874f679b40d12739534ede3dc58f0c34cdd35ac2deca4e07db"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"path": "scripts/fupload_cli/modus.py",
|
|
170
|
+
"bytes": 38821,
|
|
171
|
+
"sha256": "f5edb6898ce515a7e34f0fcab3dc022c2c5185f548644eabf5291e38b22dae22"
|
|
157
172
|
},
|
|
158
173
|
{
|
|
159
174
|
"path": "scripts/fupload_cli/newbee_auth.py",
|
|
@@ -167,8 +182,13 @@
|
|
|
167
182
|
},
|
|
168
183
|
{
|
|
169
184
|
"path": "scripts/fupload_cli/schema.py",
|
|
170
|
-
"bytes":
|
|
171
|
-
"sha256": "
|
|
185
|
+
"bytes": 46291,
|
|
186
|
+
"sha256": "025d6f4af1e8adda87aed14a075028c370e78bf0def6738ab303e2f1798aefd8"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"path": "scripts/fupload_cli/state_machine.py",
|
|
190
|
+
"bytes": 15325,
|
|
191
|
+
"sha256": "02f37b745bbb70860c09563b6778014bd1c69ca06ba28777415ad7ce84d02610"
|
|
172
192
|
},
|
|
173
193
|
{
|
|
174
194
|
"path": "scripts/fupload_cli/transport.py",
|
|
@@ -187,8 +207,8 @@
|
|
|
187
207
|
},
|
|
188
208
|
{
|
|
189
209
|
"path": "SKILL.md",
|
|
190
|
-
"bytes":
|
|
191
|
-
"sha256": "
|
|
210
|
+
"bytes": 25720,
|
|
211
|
+
"sha256": "2e20b91ded57f4d91dbb658abf6fedc636785cc1b3ada23f985c9241c53cd3e6"
|
|
192
212
|
}
|
|
193
213
|
]
|
|
194
214
|
}
|