@helix3/helix-cli 0.1.4-helix3.24
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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/ability.d.ts +73 -0
- package/dist/ability.js +120 -0
- package/dist/ability.js.map +1 -0
- package/dist/api.d.ts +24 -0
- package/dist/api.js +83 -0
- package/dist/api.js.map +1 -0
- package/dist/browserLogin.d.ts +22 -0
- package/dist/browserLogin.js +148 -0
- package/dist/browserLogin.js.map +1 -0
- package/dist/bundle.d.ts +15 -0
- package/dist/bundle.js +98 -0
- package/dist/bundle.js.map +1 -0
- package/dist/character/canonicalSkeleton.d.ts +3 -0
- package/dist/character/canonicalSkeleton.js +23 -0
- package/dist/character/canonicalSkeleton.js.map +1 -0
- package/dist/character/canonicalSkeletonData.d.ts +6 -0
- package/dist/character/canonicalSkeletonData.js +92 -0
- package/dist/character/canonicalSkeletonData.js.map +1 -0
- package/dist/character/esm.d.ts +1 -0
- package/dist/character/esm.js +6 -0
- package/dist/character/esm.js.map +1 -0
- package/dist/character/importCharacter.d.ts +88 -0
- package/dist/character/importCharacter.js +293 -0
- package/dist/character/importCharacter.js.map +1 -0
- package/dist/character/mat4.d.ts +29 -0
- package/dist/character/mat4.js +103 -0
- package/dist/character/mat4.js.map +1 -0
- package/dist/character/meshyToHelixMap.d.ts +2 -0
- package/dist/character/meshyToHelixMap.js +40 -0
- package/dist/character/meshyToHelixMap.js.map +1 -0
- package/dist/character/quat.d.ts +15 -0
- package/dist/character/quat.js +137 -0
- package/dist/character/quat.js.map +1 -0
- package/dist/character/stages/bakeTexture.d.ts +20 -0
- package/dist/character/stages/bakeTexture.js +396 -0
- package/dist/character/stages/bakeTexture.js.map +1 -0
- package/dist/character/stages/conform.d.ts +56 -0
- package/dist/character/stages/conform.js +292 -0
- package/dist/character/stages/conform.js.map +1 -0
- package/dist/character/stages/ktx2.d.ts +22 -0
- package/dist/character/stages/ktx2.js +97 -0
- package/dist/character/stages/ktx2.js.map +1 -0
- package/dist/character/stages/mergeLods.d.ts +15 -0
- package/dist/character/stages/mergeLods.js +42 -0
- package/dist/character/stages/mergeLods.js.map +1 -0
- package/dist/character/stages/report.d.ts +2 -0
- package/dist/character/stages/report.js +19 -0
- package/dist/character/stages/report.js.map +1 -0
- package/dist/character/stages/resizeTextures.d.ts +11 -0
- package/dist/character/stages/resizeTextures.js +62 -0
- package/dist/character/stages/resizeTextures.js.map +1 -0
- package/dist/character/stages/retarget.d.ts +49 -0
- package/dist/character/stages/retarget.js +105 -0
- package/dist/character/stages/retarget.js.map +1 -0
- package/dist/character/stages/retargetClip.d.ts +27 -0
- package/dist/character/stages/retargetClip.js +237 -0
- package/dist/character/stages/retargetClip.js.map +1 -0
- package/dist/character/stages/simplifyMesh.d.ts +15 -0
- package/dist/character/stages/simplifyMesh.js +37 -0
- package/dist/character/stages/simplifyMesh.js.map +1 -0
- package/dist/character/stages/splitFacePrimitive.d.ts +30 -0
- package/dist/character/stages/splitFacePrimitive.js +115 -0
- package/dist/character/stages/splitFacePrimitive.js.map +1 -0
- package/dist/character/stages/weldedSimplify.d.ts +32 -0
- package/dist/character/stages/weldedSimplify.js +202 -0
- package/dist/character/stages/weldedSimplify.js.map +1 -0
- package/dist/character/testutil/syntheticRig.d.ts +9 -0
- package/dist/character/testutil/syntheticRig.js +84 -0
- package/dist/character/testutil/syntheticRig.js.map +1 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +378 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +87 -0
- package/dist/init.js.map +1 -0
- package/dist/initAbility.d.ts +1 -0
- package/dist/initAbility.js +71 -0
- package/dist/initAbility.js.map +1 -0
- package/dist/install.d.ts +51 -0
- package/dist/install.js +201 -0
- package/dist/install.js.map +1 -0
- package/dist/lib.d.ts +88 -0
- package/dist/lib.js +347 -0
- package/dist/lib.js.map +1 -0
- package/dist/package.d.ts +11 -0
- package/dist/package.js +136 -0
- package/dist/package.js.map +1 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +55 -0
- package/dist/prompt.js.map +1 -0
- package/dist/resolve.d.ts +57 -0
- package/dist/resolve.js +103 -0
- package/dist/resolve.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/vendor/ability-manifest.mjs +558 -0
- package/vendor/installed-index.mjs +71 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// @helix/humanoid-character@0.2.2 — ability.json validator (frozen C3 contract).
|
|
2
|
+
// Generated by emit-schemas; vendored by @hypersoniclabs/helix-cli. Do not edit; changes require ability-v2.
|
|
3
|
+
|
|
4
|
+
// src/core/config/paths.ts
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/core/config/schema.ts
|
|
10
|
+
function levenshtein(a, b) {
|
|
11
|
+
const prev = new Array(b.length + 1);
|
|
12
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
13
|
+
for (let i = 1; i <= a.length; i++) {
|
|
14
|
+
let diag = prev[0];
|
|
15
|
+
prev[0] = i;
|
|
16
|
+
for (let j = 1; j <= b.length; j++) {
|
|
17
|
+
const tmp = prev[j];
|
|
18
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
19
|
+
diag = tmp;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return prev[b.length];
|
|
23
|
+
}
|
|
24
|
+
function nearestKey(key, candidates) {
|
|
25
|
+
let best;
|
|
26
|
+
let bestDist = Infinity;
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
const dist = levenshtein(key.toLowerCase(), candidate.toLowerCase());
|
|
29
|
+
if (dist < bestDist) {
|
|
30
|
+
bestDist = dist;
|
|
31
|
+
best = candidate;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return best !== void 0 && bestDist <= Math.max(2, Math.floor(key.length * 0.4)) ? best : void 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/bundle/abilityManifest.ts
|
|
38
|
+
var ABILITY_ID_PATTERN = "^[a-z0-9](?:[a-z0-9-]{1,48})[a-z0-9]$";
|
|
39
|
+
var SEMVER_PATTERN = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?$";
|
|
40
|
+
var SYSTEM_RANGE_PATTERN = "^[\\^~]?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?$";
|
|
41
|
+
var SKELETON_PATTERN = "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?@(0|[1-9]\\d*)$";
|
|
42
|
+
var ENTRY_PATH_PATTERN = "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.m?js$";
|
|
43
|
+
var SCHEMA_PATH_PATTERN = "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.json$";
|
|
44
|
+
var CLIP_SRC_PATTERN = "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.gl(?:b|tf)$";
|
|
45
|
+
var CHANNEL_NAME_PATTERN = "^[a-z][a-z0-9-]*$";
|
|
46
|
+
var LOCAL_NAME_PATTERN = "^[A-Za-z0-9_][A-Za-z0-9_-]*$";
|
|
47
|
+
var CONDITION_PARAM_PATTERN = "^[A-Za-z][A-Za-z0-9_.-]*$";
|
|
48
|
+
var RESERVED_CONFIG_PREFIXES = ["character", "abilities"];
|
|
49
|
+
var CONDITION_OPS = ["==", "!=", ">", "<", ">=", "<="];
|
|
50
|
+
var ACTIVATION_MODES = ["always", "manual", "contextual"];
|
|
51
|
+
var ACTIVATION_STYLES = ["toggle", "hold"];
|
|
52
|
+
var AbilityManifestError = class extends Error {
|
|
53
|
+
issues;
|
|
54
|
+
constructor(issues) {
|
|
55
|
+
super(`ability.json is invalid:
|
|
56
|
+
${issues.map((i) => ` ${i.path === "" ? "(root)" : i.path}: ${i.message}`).join("\n")}`);
|
|
57
|
+
this.name = "AbilityManifestError";
|
|
58
|
+
this.issues = issues;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function typeName(value) {
|
|
62
|
+
if (value === null) return "null";
|
|
63
|
+
if (Array.isArray(value)) return "array";
|
|
64
|
+
return typeof value;
|
|
65
|
+
}
|
|
66
|
+
function checkString(value, path, issues, rule = {}) {
|
|
67
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
68
|
+
issues.push({ path, message: `must be a non-empty string (got ${typeName(value)})` });
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
if (rule.pattern !== void 0 && !new RegExp(rule.pattern).test(value)) {
|
|
72
|
+
issues.push({ path, message: `'${value}' is not ${rule.hint ?? `a match for ${rule.pattern}`}` });
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function checkQualifiedName(value, path, issues, abilityId, what) {
|
|
78
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
79
|
+
issues.push({ path, message: `must be a non-empty string (got ${typeName(value)})` });
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
if (abilityId === void 0) return value;
|
|
83
|
+
const prefix = `${abilityId}.`;
|
|
84
|
+
if (!value.startsWith(prefix)) {
|
|
85
|
+
issues.push({ path, message: `${what} '${value}' must be namespaced '${prefix}<name>' (names carry the ability id)` });
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
const local = value.slice(prefix.length);
|
|
89
|
+
if (!new RegExp(LOCAL_NAME_PATTERN).test(local)) {
|
|
90
|
+
issues.push({ path, message: `${what} '${value}': the part after '${prefix}' must match ${LOCAL_NAME_PATTERN}` });
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function checkCondition(value, path, issues) {
|
|
96
|
+
if (!Array.isArray(value) || value.length !== 3) {
|
|
97
|
+
issues.push({ path, message: `must be a [param, op, value] triple (got ${typeName(value)})` });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const [param, op, operand] = value;
|
|
101
|
+
if (typeof param !== "string" || !new RegExp(CONDITION_PARAM_PATTERN).test(param)) {
|
|
102
|
+
issues.push({ path: `${path}[0]`, message: `must be a blackboard parameter name (got ${typeName(param)})` });
|
|
103
|
+
}
|
|
104
|
+
if (typeof op !== "string" || !CONDITION_OPS.includes(op)) {
|
|
105
|
+
issues.push({ path: `${path}[1]`, message: `must be one of: ${CONDITION_OPS.join(", ")}` });
|
|
106
|
+
}
|
|
107
|
+
if (typeof operand !== "number" && typeof operand !== "boolean") {
|
|
108
|
+
issues.push({ path: `${path}[2]`, message: `must be a number or boolean (got ${typeName(operand)})` });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function checkActivation(value, issues) {
|
|
112
|
+
if (!isPlainObject(value)) {
|
|
113
|
+
issues.push({ path: "activation", message: `must be an object with a 'mode' (got ${typeName(value)})` });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const mode = value.mode;
|
|
117
|
+
if (typeof mode !== "string" || !ACTIVATION_MODES.includes(mode)) {
|
|
118
|
+
issues.push({ path: "activation.mode", message: `must be one of: ${ACTIVATION_MODES.join(", ")}` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const allowed = mode === "always" ? ["mode"] : mode === "manual" ? ["mode", "action", "style"] : ["mode", "when"];
|
|
122
|
+
for (const key of Object.keys(value)) {
|
|
123
|
+
if (!allowed.includes(key)) issues.push({ path: `activation.${key}`, message: `not a field of mode '${mode}' (allowed: ${allowed.join(", ")})` });
|
|
124
|
+
}
|
|
125
|
+
if (mode === "manual") {
|
|
126
|
+
checkString(value.action, "activation.action", issues);
|
|
127
|
+
if (typeof value.style !== "string" || !ACTIVATION_STYLES.includes(value.style)) {
|
|
128
|
+
issues.push({ path: "activation.style", message: `must be one of: ${ACTIVATION_STYLES.join(", ")}` });
|
|
129
|
+
}
|
|
130
|
+
} else if (mode === "contextual") {
|
|
131
|
+
if (!Array.isArray(value.when) || value.when.length === 0) {
|
|
132
|
+
issues.push({ path: "activation.when", message: `must be a non-empty array of [param, op, value] conditions (got ${typeName(value.when)})` });
|
|
133
|
+
} else {
|
|
134
|
+
value.when.forEach((cond, i) => checkCondition(cond, `activation.when[${i}]`, issues));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function checkBindings(value, path, issues) {
|
|
139
|
+
if (!isPlainObject(value)) {
|
|
140
|
+
issues.push({ path, message: `must be an object (got ${typeName(value)})` });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const stringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string" && s.length > 0);
|
|
144
|
+
const numberArray = (v) => Array.isArray(v) && v.every((n) => typeof n === "number" && Number.isInteger(n) && n >= 0);
|
|
145
|
+
for (const key of Object.keys(value)) {
|
|
146
|
+
const v = value[key];
|
|
147
|
+
switch (key) {
|
|
148
|
+
case "keys":
|
|
149
|
+
if (!stringArray(v)) issues.push({ path: `${path}.keys`, message: "must be an array of KeyboardEvent.code strings" });
|
|
150
|
+
break;
|
|
151
|
+
case "composite": {
|
|
152
|
+
if (!isPlainObject(v)) {
|
|
153
|
+
issues.push({ path: `${path}.composite`, message: "must be an object with up/down/left/right key arrays" });
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
for (const dir of Object.keys(v)) {
|
|
157
|
+
if (!["up", "down", "left", "right"].includes(dir)) issues.push({ path: `${path}.composite.${dir}`, message: "must be one of: up, down, left, right" });
|
|
158
|
+
else if (!stringArray(v[dir])) issues.push({ path: `${path}.composite.${dir}`, message: "must be an array of KeyboardEvent.code strings" });
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "mouseButtons":
|
|
163
|
+
case "gamepadButtons":
|
|
164
|
+
if (!numberArray(v)) issues.push({ path: `${path}.${key}`, message: "must be an array of non-negative button indices" });
|
|
165
|
+
break;
|
|
166
|
+
case "gamepadAxes":
|
|
167
|
+
if (!numberArray(v) || v.length !== 2) issues.push({ path: `${path}.gamepadAxes`, message: "must be [xAxisIndex, yAxisIndex]" });
|
|
168
|
+
break;
|
|
169
|
+
case "deadzone":
|
|
170
|
+
if (typeof v !== "number" || v < 0 || v >= 1) issues.push({ path: `${path}.deadzone`, message: "must be a number in [0, 1)" });
|
|
171
|
+
break;
|
|
172
|
+
default: {
|
|
173
|
+
const suggestion = nearestKey(key, ["keys", "composite", "mouseButtons", "gamepadButtons", "gamepadAxes", "deadzone"]);
|
|
174
|
+
issues.push({ path: `${path}.${key}`, message: suggestion !== void 0 ? `unknown binding field \u2014 did you mean "${suggestion}"?` : "unknown binding field" });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function validateAbilityManifest(value) {
|
|
180
|
+
const issues = [];
|
|
181
|
+
if (!isPlainObject(value)) {
|
|
182
|
+
issues.push({ path: "", message: `ability.json must be a JSON object (got ${typeName(value)})` });
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
185
|
+
const knownFields = Object.keys(ABILITY_V1_JSON_SCHEMA.properties);
|
|
186
|
+
for (const key of Object.keys(value)) {
|
|
187
|
+
if (!knownFields.includes(key)) {
|
|
188
|
+
const suggestion = nearestKey(key, knownFields);
|
|
189
|
+
issues.push({ path: key, message: suggestion !== void 0 ? `unknown field \u2014 did you mean "${suggestion}"?` : `unknown field (allowed: ${knownFields.join(", ")})` });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (value.schemaVersion !== 1) {
|
|
193
|
+
issues.push({ path: "schemaVersion", message: `must be the number 1 \u2014 this validator implements ability-v1 (got ${JSON.stringify(value.schemaVersion)})` });
|
|
194
|
+
}
|
|
195
|
+
const id = checkString(value.id, "id", issues, {
|
|
196
|
+
pattern: ABILITY_ID_PATTERN,
|
|
197
|
+
hint: "a platform slug (3-50 chars: lowercase letters, digits, hyphens; no leading/trailing hyphen)"
|
|
198
|
+
});
|
|
199
|
+
if (id !== void 0 && RESERVED_CONFIG_PREFIXES.includes(id)) {
|
|
200
|
+
issues.push({ path: "id", message: `'${id}' is a reserved config prefix (reserved: ${RESERVED_CONFIG_PREFIXES.join(", ")})` });
|
|
201
|
+
}
|
|
202
|
+
checkString(value.version, "version", issues, { pattern: SEMVER_PATTERN, hint: "a semver version like 1.0.0" });
|
|
203
|
+
checkString(value.title, "title", issues);
|
|
204
|
+
if (value.description !== void 0) checkString(value.description, "description", issues);
|
|
205
|
+
if (!isPlainObject(value.system)) {
|
|
206
|
+
issues.push({ path: "system", message: `must be the host-system pin { id, version, skeleton } (got ${typeName(value.system)})` });
|
|
207
|
+
} else {
|
|
208
|
+
checkString(value.system.id, "system.id", issues, { pattern: ABILITY_ID_PATTERN, hint: "a system slug like 'humanoid-character'" });
|
|
209
|
+
checkString(value.system.version, "system.version", issues, { pattern: SYSTEM_RANGE_PATTERN, hint: "a semver range like '^1.0.0' (exact, ^, or ~)" });
|
|
210
|
+
checkString(value.system.skeleton, "system.skeleton", issues, { pattern: SKELETON_PATTERN, hint: "a skeleton standard tag like 'helix-humanoid@1'" });
|
|
211
|
+
for (const key of Object.keys(value.system)) {
|
|
212
|
+
if (!["id", "version", "skeleton"].includes(key)) issues.push({ path: `system.${key}`, message: "unknown field (allowed: id, version, skeleton)" });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (value.activation === void 0) issues.push({ path: "activation", message: "required \u2014 one of { mode: 'always' | 'manual' | 'contextual' }" });
|
|
216
|
+
else checkActivation(value.activation, issues);
|
|
217
|
+
if (value.channels !== void 0) {
|
|
218
|
+
if (!isPlainObject(value.channels)) {
|
|
219
|
+
issues.push({ path: "channels", message: `must be an object of { <channel>: { priority } } (got ${typeName(value.channels)})` });
|
|
220
|
+
} else {
|
|
221
|
+
for (const [channel, spec] of Object.entries(value.channels)) {
|
|
222
|
+
if (!new RegExp(CHANNEL_NAME_PATTERN).test(channel)) {
|
|
223
|
+
issues.push({ path: `channels.${channel}`, message: `channel names must match ${CHANNEL_NAME_PATTERN}` });
|
|
224
|
+
}
|
|
225
|
+
if (!isPlainObject(spec) || typeof spec.priority !== "number" || !Number.isFinite(spec.priority) || spec.priority < 0) {
|
|
226
|
+
issues.push({ path: `channels.${channel}`, message: "must be { priority: <number >= 0> }" });
|
|
227
|
+
} else {
|
|
228
|
+
for (const key of Object.keys(spec)) {
|
|
229
|
+
if (key !== "priority") issues.push({ path: `channels.${channel}.${key}`, message: "unknown field (allowed: priority)" });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const actionIds = [];
|
|
236
|
+
if (value.actions !== void 0) {
|
|
237
|
+
if (!Array.isArray(value.actions)) {
|
|
238
|
+
issues.push({ path: "actions", message: `must be an array of { id, kind?, defaultBinding? } (got ${typeName(value.actions)})` });
|
|
239
|
+
} else {
|
|
240
|
+
value.actions.forEach((action, i) => {
|
|
241
|
+
const path = `actions[${i}]`;
|
|
242
|
+
if (!isPlainObject(action)) {
|
|
243
|
+
issues.push({ path, message: `must be an object (got ${typeName(action)})` });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const actionId = checkQualifiedName(action.id, `${path}.id`, issues, id, "action");
|
|
247
|
+
if (actionId !== void 0) {
|
|
248
|
+
if (actionIds.includes(actionId)) issues.push({ path: `${path}.id`, message: `duplicate action id '${actionId}'` });
|
|
249
|
+
actionIds.push(actionId);
|
|
250
|
+
}
|
|
251
|
+
if (action.kind !== void 0 && (typeof action.kind !== "string" || !["button", "toggle", "vec2"].includes(action.kind))) {
|
|
252
|
+
issues.push({ path: `${path}.kind`, message: "must be one of: button, toggle, vec2" });
|
|
253
|
+
}
|
|
254
|
+
if (action.defaultBinding !== void 0) checkBindings(action.defaultBinding, `${path}.defaultBinding`, issues);
|
|
255
|
+
for (const key of Object.keys(action)) {
|
|
256
|
+
if (!["id", "kind", "defaultBinding"].includes(key)) issues.push({ path: `${path}.${key}`, message: "unknown field (allowed: id, kind, defaultBinding)" });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const clipNames = [];
|
|
262
|
+
const additiveRefs = [];
|
|
263
|
+
if (value.clips !== void 0) {
|
|
264
|
+
if (!Array.isArray(value.clips)) {
|
|
265
|
+
issues.push({ path: "clips", message: `must be an array of { name, src, ... } (got ${typeName(value.clips)})` });
|
|
266
|
+
} else {
|
|
267
|
+
value.clips.forEach((clip, i) => {
|
|
268
|
+
const path = `clips[${i}]`;
|
|
269
|
+
if (!isPlainObject(clip)) {
|
|
270
|
+
issues.push({ path, message: `must be an object (got ${typeName(clip)})` });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const name = checkQualifiedName(clip.name, `${path}.name`, issues, id, "clip");
|
|
274
|
+
if (name !== void 0) {
|
|
275
|
+
if (clipNames.includes(name)) issues.push({ path: `${path}.name`, message: `duplicate clip name '${name}'` });
|
|
276
|
+
clipNames.push(name);
|
|
277
|
+
}
|
|
278
|
+
checkString(clip.src, `${path}.src`, issues, { pattern: CLIP_SRC_PATTERN, hint: "a bundle-relative .glb/.gltf path (no leading '/', no '..')" });
|
|
279
|
+
if (clip.loop !== void 0 && typeof clip.loop !== "boolean") issues.push({ path: `${path}.loop`, message: "must be a boolean" });
|
|
280
|
+
if (clip.strideSpeed !== void 0 && clip.strideSpeed !== null && typeof clip.strideSpeed !== "number") {
|
|
281
|
+
issues.push({ path: `${path}.strideSpeed`, message: "must be a number (m/s) or null for non-travelling clips" });
|
|
282
|
+
}
|
|
283
|
+
if (clip.additive !== void 0 && typeof clip.additive !== "boolean") issues.push({ path: `${path}.additive`, message: "must be a boolean" });
|
|
284
|
+
if (clip.additiveRef !== void 0) {
|
|
285
|
+
const ref = checkQualifiedName(clip.additiveRef, `${path}.additiveRef`, issues, id, "clip reference");
|
|
286
|
+
if (ref !== void 0) additiveRefs.push([i, ref]);
|
|
287
|
+
}
|
|
288
|
+
if (clip.direction !== void 0 && typeof clip.direction !== "number") issues.push({ path: `${path}.direction`, message: "must be a number (deg, gait-F = 0)" });
|
|
289
|
+
for (const key of Object.keys(clip)) {
|
|
290
|
+
if (!["name", "src", "loop", "strideSpeed", "additive", "additiveRef", "direction"].includes(key)) {
|
|
291
|
+
const suggestion = nearestKey(key, ["name", "src", "loop", "strideSpeed", "additive", "additiveRef", "direction"]);
|
|
292
|
+
issues.push({ path: `${path}.${key}`, message: suggestion !== void 0 ? `unknown field \u2014 did you mean "${suggestion}"?` : "unknown field" });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
for (const [i, ref] of additiveRefs) {
|
|
297
|
+
if (!clipNames.includes(ref)) {
|
|
298
|
+
issues.push({ path: `clips[${i}].additiveRef`, message: `'${ref}' does not name a clip in this manifest (clips: ${clipNames.join(", ") || "none"})` });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (isPlainObject(value.activation) && value.activation.mode === "manual" && typeof value.activation.action === "string") {
|
|
304
|
+
if (!actionIds.includes(value.activation.action)) {
|
|
305
|
+
issues.push({
|
|
306
|
+
path: "activation.action",
|
|
307
|
+
message: `'${value.activation.action}' is not declared in actions (declared: ${actionIds.join(", ") || "none"})`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
checkString(value.configSchema, "configSchema", issues, { pattern: SCHEMA_PATH_PATTERN, hint: "a bundle-relative .json path (no leading '/', no '..')" });
|
|
312
|
+
checkString(value.entry, "entry", issues, { pattern: ENTRY_PATH_PATTERN, hint: "a bundle-relative .js/.mjs path (no leading '/', no '..')" });
|
|
313
|
+
return issues;
|
|
314
|
+
}
|
|
315
|
+
function parseAbilityManifest(value) {
|
|
316
|
+
const issues = validateAbilityManifest(value);
|
|
317
|
+
if (issues.length > 0) throw new AbilityManifestError(issues);
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function parseSemver(version) {
|
|
321
|
+
const match = new RegExp(SEMVER_PATTERN).exec(version);
|
|
322
|
+
if (match === null) return null;
|
|
323
|
+
const prerelease = version.includes("-") ? version.slice(version.indexOf("-") + 1).split(".") : [];
|
|
324
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease };
|
|
325
|
+
}
|
|
326
|
+
function compareSemver(a, b) {
|
|
327
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1;
|
|
328
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
|
|
329
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
|
|
330
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
331
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
332
|
+
}
|
|
333
|
+
for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i++) {
|
|
334
|
+
const ai = a.prerelease[i];
|
|
335
|
+
const bi = b.prerelease[i];
|
|
336
|
+
if (ai === void 0) return -1;
|
|
337
|
+
if (bi === void 0) return 1;
|
|
338
|
+
const an = /^\d+$/.test(ai) ? Number(ai) : null;
|
|
339
|
+
const bn = /^\d+$/.test(bi) ? Number(bi) : null;
|
|
340
|
+
if (an !== null && bn !== null) {
|
|
341
|
+
if (an !== bn) return an < bn ? -1 : 1;
|
|
342
|
+
} else if (an !== null) return -1;
|
|
343
|
+
else if (bn !== null) return 1;
|
|
344
|
+
else if (ai !== bi) return ai < bi ? -1 : 1;
|
|
345
|
+
}
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
function satisfiesSystemPin(systemVersion, range) {
|
|
349
|
+
const op = range.startsWith("^") ? "^" : range.startsWith("~") ? "~" : "";
|
|
350
|
+
const base = parseSemver(op === "" ? range : range.slice(1));
|
|
351
|
+
const version = parseSemver(systemVersion);
|
|
352
|
+
if (base === null || version === null) return false;
|
|
353
|
+
if (op === "") return compareSemver(version, base) === 0;
|
|
354
|
+
if (version.prerelease.length > 0) {
|
|
355
|
+
const sameTuple = version.major === base.major && version.minor === base.minor && version.patch === base.patch;
|
|
356
|
+
if (!sameTuple || base.prerelease.length === 0) return false;
|
|
357
|
+
}
|
|
358
|
+
if (compareSemver(version, base) < 0) return false;
|
|
359
|
+
if (op === "~") return version.major === base.major && version.minor === base.minor;
|
|
360
|
+
if (base.major > 0) return version.major === base.major;
|
|
361
|
+
if (base.minor > 0) return version.major === 0 && version.minor === base.minor;
|
|
362
|
+
return version.major === 0 && version.minor === 0 && version.patch === base.patch;
|
|
363
|
+
}
|
|
364
|
+
var ABILITY_V1_JSON_SCHEMA = {
|
|
365
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
366
|
+
$id: "https://helixb.dev/schemas/ability-v1.schema.json",
|
|
367
|
+
title: "HELIX Ability Bundle Manifest v1",
|
|
368
|
+
description: 'The system-owned payload of a kind:ability platform bundle; lives at the bundle root as ability.json and is the single authoring surface \u2014 the platform envelope (helix.json) is derived from it at publish. Beyond this schema, the reference validator (@helix/humanoid-character/ability-manifest) enforces: action ids and clip names are namespaced "<id>.<name>"; a manual activation action must be declared in actions; additiveRef must name a clip in this manifest; action ids and clip names are unique; the ability id may not be a reserved config prefix (character, abilities).',
|
|
369
|
+
type: "object",
|
|
370
|
+
additionalProperties: false,
|
|
371
|
+
required: ["schemaVersion", "id", "version", "title", "system", "activation", "configSchema", "entry"],
|
|
372
|
+
properties: {
|
|
373
|
+
$schema: { type: "string", description: "Editor convenience \u2014 ignored by the validator." },
|
|
374
|
+
schemaVersion: { const: 1, description: "Payload schema version. Post-freeze changes bump this (ability-v2), never edit v1." },
|
|
375
|
+
id: {
|
|
376
|
+
type: "string",
|
|
377
|
+
pattern: ABILITY_ID_PATTERN,
|
|
378
|
+
description: "Ability id (platform slug rules: 3-50 chars, lowercase letters/digits/hyphens, no leading/trailing hyphen). Namespaces the config mount (<id>.*), clip names, and action ids. Becomes the platform slug at publish."
|
|
379
|
+
},
|
|
380
|
+
version: { type: "string", pattern: SEMVER_PATTERN, description: "Semver version of this ability bundle." },
|
|
381
|
+
title: { type: "string", minLength: 1, description: "Display name for catalogs and agent discovery." },
|
|
382
|
+
description: { type: "string", description: "What the ability does \u2014 agent- and human-facing." },
|
|
383
|
+
system: {
|
|
384
|
+
type: "object",
|
|
385
|
+
additionalProperties: false,
|
|
386
|
+
required: ["id", "version", "skeleton"],
|
|
387
|
+
description: "The host system this ability targets. Validated by the CLI at install and by the runtime at load; a mismatch refuses the bundle.",
|
|
388
|
+
properties: {
|
|
389
|
+
id: { type: "string", pattern: ABILITY_ID_PATTERN, description: "System id, e.g. 'humanoid-character'." },
|
|
390
|
+
version: {
|
|
391
|
+
type: "string",
|
|
392
|
+
pattern: SYSTEM_RANGE_PATTERN,
|
|
393
|
+
description: "Semver range the running system version must satisfy: exact 'x.y.z', caret '^x.y.z', or tilde '~x.y.z' (npm semantics)."
|
|
394
|
+
},
|
|
395
|
+
skeleton: { type: "string", pattern: SKELETON_PATTERN, description: "Skeleton standard tag, exact-matched at load, e.g. 'helix-humanoid@1'." }
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
activation: {
|
|
399
|
+
description: "How the ability becomes active. 'always': activated at world load. 'manual': the manager auto-binds the declared action and toggles/holds activation on it. 'contextual': blackboard conditions (the AnimGraph condition grammar) evaluated per tick \u2014 enter-activate, exit-deactivate. Channel claims bind to activation, not install.",
|
|
400
|
+
oneOf: [
|
|
401
|
+
{
|
|
402
|
+
type: "object",
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
required: ["mode"],
|
|
405
|
+
properties: { mode: { const: "always" } }
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
type: "object",
|
|
409
|
+
additionalProperties: false,
|
|
410
|
+
required: ["mode", "action", "style"],
|
|
411
|
+
properties: {
|
|
412
|
+
mode: { const: "manual" },
|
|
413
|
+
action: { type: "string", description: "A qualified action id declared in actions, e.g. 'fly.toggle'." },
|
|
414
|
+
style: { enum: [...ACTIVATION_STYLES], description: "'toggle' flips activation on press; 'hold' activates while held." }
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
type: "object",
|
|
419
|
+
additionalProperties: false,
|
|
420
|
+
required: ["mode", "when"],
|
|
421
|
+
properties: {
|
|
422
|
+
mode: { const: "contextual" },
|
|
423
|
+
when: {
|
|
424
|
+
type: "array",
|
|
425
|
+
minItems: 1,
|
|
426
|
+
description: 'AND-ed [param, op, value] conditions over blackboard parameters, e.g. [["inWater", "==", true]]. Parameters the world must publish are part of the ability contract \u2014 document them in description.',
|
|
427
|
+
items: {
|
|
428
|
+
type: "array",
|
|
429
|
+
minItems: 3,
|
|
430
|
+
maxItems: 3,
|
|
431
|
+
prefixItems: [
|
|
432
|
+
{ type: "string", pattern: CONDITION_PARAM_PATTERN, description: "Blackboard parameter name." },
|
|
433
|
+
{ enum: [...CONDITION_OPS], description: "Comparison operator." },
|
|
434
|
+
{ type: ["number", "boolean"], description: "Comparison operand." }
|
|
435
|
+
],
|
|
436
|
+
items: false
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
443
|
+
channels: {
|
|
444
|
+
type: "object",
|
|
445
|
+
description: 'Exclusive channels claimed when the ability activates and released when it deactivates, e.g. { "movement": { "priority": 10 } }. Higher priority displaces (suspends) lower; the displaced holder resumes when the channel frees.',
|
|
446
|
+
propertyNames: { pattern: CHANNEL_NAME_PATTERN },
|
|
447
|
+
additionalProperties: {
|
|
448
|
+
type: "object",
|
|
449
|
+
additionalProperties: false,
|
|
450
|
+
required: ["priority"],
|
|
451
|
+
properties: { priority: { type: "number", minimum: 0, description: "Claim priority; built-in locomotion holds movement at 0." } }
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
actions: {
|
|
455
|
+
type: "array",
|
|
456
|
+
description: "Input actions registered at install. Ids are namespaced by the ability id.",
|
|
457
|
+
items: {
|
|
458
|
+
type: "object",
|
|
459
|
+
additionalProperties: false,
|
|
460
|
+
required: ["id"],
|
|
461
|
+
properties: {
|
|
462
|
+
id: { type: "string", description: "Qualified action id '<abilityId>.<action>', e.g. 'fly.toggle'." },
|
|
463
|
+
kind: { enum: ["button", "toggle", "vec2"], default: "button", description: "Action kind (mirrors the system input service)." },
|
|
464
|
+
defaultBinding: {
|
|
465
|
+
type: "object",
|
|
466
|
+
additionalProperties: false,
|
|
467
|
+
description: "Default KB/M + standard-gamepad bindings; omitted = unbound until rebound. Mirrors the system ActionBindings shape.",
|
|
468
|
+
properties: {
|
|
469
|
+
keys: { type: "array", items: { type: "string" }, description: "KeyboardEvent.code values." },
|
|
470
|
+
composite: {
|
|
471
|
+
type: "object",
|
|
472
|
+
additionalProperties: false,
|
|
473
|
+
description: "Keyboard composite for vec2 actions; +y is forward, +x is right.",
|
|
474
|
+
properties: {
|
|
475
|
+
up: { type: "array", items: { type: "string" } },
|
|
476
|
+
down: { type: "array", items: { type: "string" } },
|
|
477
|
+
left: { type: "array", items: { type: "string" } },
|
|
478
|
+
right: { type: "array", items: { type: "string" } }
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
mouseButtons: { type: "array", items: { type: "integer", minimum: 0 }, description: "MouseEvent.button values (0 = left, 2 = right)." },
|
|
482
|
+
gamepadButtons: { type: "array", items: { type: "integer", minimum: 0 }, description: "Standard-mapping gamepad button indices." },
|
|
483
|
+
gamepadAxes: {
|
|
484
|
+
type: "array",
|
|
485
|
+
minItems: 2,
|
|
486
|
+
maxItems: 2,
|
|
487
|
+
items: { type: "integer", minimum: 0 },
|
|
488
|
+
description: "[xAxisIndex, yAxisIndex] into Gamepad.axes."
|
|
489
|
+
},
|
|
490
|
+
deadzone: { type: "number", minimum: 0, exclusiveMaximum: 1, description: "Radial stick deadzone, default 0.15." }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
clips: {
|
|
497
|
+
type: "array",
|
|
498
|
+
description: "Animation clips fetched and registered at load. Names are namespaced by the ability id; meta fields mirror clip-meta.json.",
|
|
499
|
+
items: {
|
|
500
|
+
type: "object",
|
|
501
|
+
additionalProperties: false,
|
|
502
|
+
required: ["name", "src"],
|
|
503
|
+
properties: {
|
|
504
|
+
name: { type: "string", description: "Qualified clip name '<abilityId>.<clip>', e.g. 'fly.hover'." },
|
|
505
|
+
src: { type: "string", pattern: CLIP_SRC_PATTERN, description: "Bundle-relative .glb/.gltf path (no leading '/', no '..')." },
|
|
506
|
+
loop: { type: "boolean", default: true },
|
|
507
|
+
strideSpeed: { type: ["number", "null"], description: "Measured ground speed (m/s) of the source motion; null for non-travelling clips." },
|
|
508
|
+
additive: { type: "boolean", description: "Tracks are pre-baked deltas \u2014 bound with additive blend." },
|
|
509
|
+
additiveRef: { type: "string", description: "Qualified name of another clip in this manifest to derive the additive delta against." },
|
|
510
|
+
direction: { type: "number", description: "Signed travel direction (deg, gait-F = 0) for directional locomotion clips." }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
configSchema: {
|
|
515
|
+
type: "string",
|
|
516
|
+
pattern: SCHEMA_PATH_PATTERN,
|
|
517
|
+
description: "Bundle-relative path to the JSON Schema for this ability's config, mounted at the ability id (fly.maxSpeed). Mandatory \u2014 the manifest is the documentation agents discover tunables from."
|
|
518
|
+
},
|
|
519
|
+
entry: {
|
|
520
|
+
type: "string",
|
|
521
|
+
pattern: ENTRY_PATH_PATTERN,
|
|
522
|
+
description: "Bundle-relative ESM entry; its default export is a class implementing the system Ability interface."
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/core/identity.ts
|
|
528
|
+
var SYSTEM_ID = "humanoid-character";
|
|
529
|
+
var SYSTEM_VERSION = "0.2.2";
|
|
530
|
+
var SKELETON_STANDARD = "helix-humanoid@1";
|
|
531
|
+
var SYSTEM_IDENTITY = { id: SYSTEM_ID, version: SYSTEM_VERSION, skeleton: SKELETON_STANDARD };
|
|
532
|
+
export {
|
|
533
|
+
ABILITY_ID_PATTERN,
|
|
534
|
+
ABILITY_V1_JSON_SCHEMA,
|
|
535
|
+
ACTIVATION_MODES,
|
|
536
|
+
ACTIVATION_STYLES,
|
|
537
|
+
AbilityManifestError,
|
|
538
|
+
CHANNEL_NAME_PATTERN,
|
|
539
|
+
CLIP_SRC_PATTERN,
|
|
540
|
+
CONDITION_OPS,
|
|
541
|
+
CONDITION_PARAM_PATTERN,
|
|
542
|
+
ENTRY_PATH_PATTERN,
|
|
543
|
+
LOCAL_NAME_PATTERN,
|
|
544
|
+
RESERVED_CONFIG_PREFIXES,
|
|
545
|
+
SCHEMA_PATH_PATTERN,
|
|
546
|
+
SEMVER_PATTERN,
|
|
547
|
+
SKELETON_PATTERN,
|
|
548
|
+
SKELETON_STANDARD,
|
|
549
|
+
SYSTEM_ID,
|
|
550
|
+
SYSTEM_IDENTITY,
|
|
551
|
+
SYSTEM_RANGE_PATTERN,
|
|
552
|
+
SYSTEM_VERSION,
|
|
553
|
+
compareSemver,
|
|
554
|
+
parseAbilityManifest,
|
|
555
|
+
parseSemver,
|
|
556
|
+
satisfiesSystemPin,
|
|
557
|
+
validateAbilityManifest
|
|
558
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @helix/humanoid-character@0.2.2 — installed.json (install-index) validator.
|
|
2
|
+
// Consumer/loader contract: VERSIONED and NON-frozen (NOT ability-v1). Generated by emit-schemas; regenerate freely.
|
|
3
|
+
|
|
4
|
+
// src/bundle/installedIndex.ts
|
|
5
|
+
var ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,48})[a-z0-9]$/;
|
|
6
|
+
var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/;
|
|
7
|
+
var REL_PATH_PATTERN = /^(?!\/)(?!.*\.\.)[A-Za-z0-9._/-]+$/;
|
|
8
|
+
var ABS_URL_PATTERN = /^https?:\/\/\S+$/;
|
|
9
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "version", "codeUrl", "assetBaseUrl", "path"]);
|
|
10
|
+
function typeName(v) {
|
|
11
|
+
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
|
|
12
|
+
}
|
|
13
|
+
function isResolvedEntry(entry) {
|
|
14
|
+
return typeof entry.codeUrl === "string";
|
|
15
|
+
}
|
|
16
|
+
function validCodeUrl(v) {
|
|
17
|
+
return typeof v === "string" && !v.includes("..") && (ABS_URL_PATTERN.test(v) || REL_PATH_PATTERN.test(v));
|
|
18
|
+
}
|
|
19
|
+
function validateInstalledIndex(value) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
issues.push({ path: "", message: `installed.json must be an array of entries (got ${typeName(value)})` });
|
|
23
|
+
return issues;
|
|
24
|
+
}
|
|
25
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26
|
+
value.forEach((entry, i) => {
|
|
27
|
+
const at = (k) => k ? `[${i}].${k}` : `[${i}]`;
|
|
28
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
29
|
+
issues.push({ path: at(""), message: `must be an object (got ${typeName(entry)})` });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const e = entry;
|
|
33
|
+
if (typeof e.id !== "string" || !ID_PATTERN.test(e.id)) {
|
|
34
|
+
issues.push({ path: at("id"), message: "must be a platform slug (3-50 chars: a-z, 0-9, hyphen; no leading/trailing hyphen)" });
|
|
35
|
+
} else {
|
|
36
|
+
if (seen.has(e.id)) issues.push({ path: at("id"), message: `duplicate ability id '${e.id}'` });
|
|
37
|
+
seen.add(e.id);
|
|
38
|
+
}
|
|
39
|
+
if (typeof e.version !== "string" || !SEMVER_PATTERN.test(e.version)) {
|
|
40
|
+
issues.push({ path: at("version"), message: "must be a semver version like 1.0.0" });
|
|
41
|
+
}
|
|
42
|
+
const hasResolved = "codeUrl" in e || "assetBaseUrl" in e;
|
|
43
|
+
const hasLocal = "path" in e;
|
|
44
|
+
if (hasResolved && hasLocal) {
|
|
45
|
+
issues.push({ path: at(""), message: "entry must be EITHER { codeUrl, assetBaseUrl } (resolved) OR { path } (local), not both" });
|
|
46
|
+
} else if (hasResolved) {
|
|
47
|
+
if (!validCodeUrl(e.codeUrl)) {
|
|
48
|
+
issues.push({ path: at("codeUrl"), message: 'must be an absolute http(s) URL (CDN) or an index-relative path like "fly" (no leading "/", no "..")' });
|
|
49
|
+
}
|
|
50
|
+
if (typeof e.assetBaseUrl !== "string" || !ABS_URL_PATTERN.test(e.assetBaseUrl)) {
|
|
51
|
+
issues.push({ path: at("assetBaseUrl"), message: "must be an absolute http(s) URL \u2014 assets always stream from the CDN" });
|
|
52
|
+
}
|
|
53
|
+
} else if (hasLocal) {
|
|
54
|
+
if (typeof e.path !== "string" || !REL_PATH_PATTERN.test(e.path)) {
|
|
55
|
+
issues.push({ path: at("path"), message: "must be an index-relative path (no leading '/', no '..')" });
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
issues.push({ path: at(""), message: "entry needs { codeUrl, assetBaseUrl } (resolved) or { path } (local)" });
|
|
59
|
+
}
|
|
60
|
+
for (const key of Object.keys(e)) {
|
|
61
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
62
|
+
issues.push({ path: at(key), message: "unknown field (allowed: id, version, codeUrl, assetBaseUrl, path)" });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
isResolvedEntry,
|
|
70
|
+
validateInstalledIndex
|
|
71
|
+
};
|