@go-labs-sg/gosh 0.12.0 → 0.14.0
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 +55 -3
- package/command-manifest.json +19 -0
- package/command-reference.md +16 -0
- package/dist/index.js +38 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Commands use `--input '<json>'` for the exact validated tRPC input object. This
|
|
|
97
97
|
|
|
98
98
|
`gosh event list-page` returns `{ items, page, pageSize, pageCount, totalCount }`. It defaults to page 1 with 25 results, accepts page sizes from 1 to 100, and supports server-side `search` and `status` filters. Archived events are excluded unless an explicit status is supplied. Request subsequent pages explicitly; the CLI does not automatically fetch every page. The existing `gosh event list` command and its array response remain unchanged.
|
|
99
99
|
|
|
100
|
-
The command registry exposes the supported GOSH tRPC procedures across organizer and global-admin operations, organization invitations and membership, player workflows, public results/login procedures, uploads, and realtime subscriptions. Update the registry alongside API changes, then run `bun
|
|
100
|
+
The command registry exposes the supported GOSH tRPC procedures across organizer and global-admin operations, organization invitations and membership, player workflows, public results/login procedures, uploads, and realtime subscriptions. Update the registry alongside API changes, then run `bun --filter @go-labs-sg/gosh generate:commands` from the repository root to regenerate the manifest, reference, and typed operations.
|
|
101
101
|
|
|
102
102
|
### Templates and organization scope
|
|
103
103
|
|
|
@@ -183,8 +183,9 @@ value in that template team's `order` field:
|
|
|
183
183
|
Events default to `OPEN` progression, which keeps the existing open-play flow.
|
|
184
184
|
Sequential progression starts each team or individual on an ordered station
|
|
185
185
|
route. Group routing is available only for `TEAM` play mode. Create teams and
|
|
186
|
-
assign them to routing groups before launching the event; setup
|
|
187
|
-
play starts.
|
|
186
|
+
assign them to routing groups before launching the event; sequential setup
|
|
187
|
+
freezes when play starts. Grouped OPEN events lock only a started team's group
|
|
188
|
+
assignment, a started group's route and the gameplay settings. A group shares its ordered station route, while each team keeps
|
|
188
189
|
independent progress through that route.
|
|
189
190
|
|
|
190
191
|
Configure gameplay, then manage the event's routing groups and their complete
|
|
@@ -225,6 +226,43 @@ the desired order. Use `groupId: null` with `assign-team` to unassign a team.
|
|
|
225
226
|
Group names are trimmed and limited to 80 characters. The delete command
|
|
226
227
|
requires the additional `--allow-delete` effect flag.
|
|
227
228
|
|
|
229
|
+
### Staggered group release
|
|
230
|
+
|
|
231
|
+
`routing-group release` changes flag-off without changing routes. It requires
|
|
232
|
+
launch permission and a draft, ready or live TEAM event with group routing. A
|
|
233
|
+
different group already playing does not block it. Once a team in the target
|
|
234
|
+
group has started, only HOLD (emergency hold) and RELEASE_SCHEDULED_HOLD (resume)
|
|
235
|
+
are accepted.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
gosh routing-group release \
|
|
239
|
+
--input '{"eventId":"event-id","groupId":"group-d","action":"SCHEDULE","releaseAt":"2030-01-01T09:20:00+08:00"}' \
|
|
240
|
+
--target group-d \
|
|
241
|
+
--allow-state-change
|
|
242
|
+
gosh routing-group release \
|
|
243
|
+
--input '{"eventId":"event-id","groupId":"group-d","action":"HOLD"}' \
|
|
244
|
+
--target group-d \
|
|
245
|
+
--allow-state-change
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Replace example IDs/time with your intended target and a future instant.
|
|
249
|
+
SCHEDULE requires an ISO timestamp with `Z` or a numeric timezone offset;
|
|
250
|
+
offsetless, invalid and past times are rejected. CLEAR sets the schedule to null
|
|
251
|
+
but preserves a hold. HOLD overrides the clock. RELEASE_SCHEDULED_HOLD removes
|
|
252
|
+
the hold and retains the schedule; an elapsed/no schedule releases immediately.
|
|
253
|
+
RELEASE_NOW clears both schedule and hold. Omit `releaseAt` for non-schedule
|
|
254
|
+
actions. Repeated no-op controls do not create duplicate audits. List routing
|
|
255
|
+
groups to read back release values and editability, and use
|
|
256
|
+
`gosh routing-group release-history --input '{"eventId":"event-id","groupId":"group-d"}'`
|
|
257
|
+
for the five most recent actor/before/after changes; the existing event activity
|
|
258
|
+
timeline provides the full audit history.
|
|
259
|
+
|
|
260
|
+
The server evaluates deadlines without cron. Release never starts a timer or
|
|
261
|
+
bypasses LIVE status, availability, check-in or captain permissions. Any team in
|
|
262
|
+
a group beginning play locks that group's schedule; emergency HOLD/resume stays
|
|
263
|
+
available and does not pause running station clocks. There is no
|
|
264
|
+
untimed practice-mission exemption. See [the design notes](../../../docs/design/gosh-staggered-routing-group-release.md).
|
|
265
|
+
|
|
228
266
|
For sequential player actions, complete any required location check-in with
|
|
229
267
|
`gosh player station-check-in` first. The server checks the signed-in player’s
|
|
230
268
|
current check-in record; a successful check-in also starts the station timer.
|
|
@@ -358,6 +396,20 @@ Without the allow flags, an interactive terminal prints a workflow preview and r
|
|
|
358
396
|
|
|
359
397
|
One-shot commands write one JSON envelope to stdout. Watch commands write newline-delimited JSON envelopes. Failures and opt-in `--debug` diagnostics go to stderr, and known secrets, payloads, join codes, share tokens, and signed URLs are redacted. `--quiet` or `-q` suppresses diagnostics.
|
|
360
398
|
|
|
399
|
+
## Monorepo development
|
|
400
|
+
|
|
401
|
+
Follow the [development workflow](../../../docs/development-workflow.md#required-cli-coverage). App/API changes affecting this CLI must update its commands, types/input-output contracts, help/docs and verification in the same change. Use the existing authenticated product API rather than CLI-only business logic. Presentation-only changes do not require CLI edits; record why when no CLI update was needed.
|
|
402
|
+
|
|
403
|
+
From the repository root after isolated-worktree initialization:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
bun --filter @go-labs-sg/gosh typecheck
|
|
407
|
+
bun --filter @go-labs-sg/gosh build
|
|
408
|
+
bun run packages/gosh/cli/src/index.ts help
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Building is not publication or permission to mutate live data. Use focused temporary business-behavior tests and the final pre-push gate from the shared workflow. Its existing build regenerates command artifacts; review the manifest/reference and typed operations after registry changes.
|
|
412
|
+
|
|
361
413
|
## Release verification
|
|
362
414
|
|
|
363
415
|
The `gosh-cli-v*` release workflow runs
|
package/command-manifest.json
CHANGED
|
@@ -1173,6 +1173,25 @@
|
|
|
1173
1173
|
"requiresAuth": true,
|
|
1174
1174
|
"summary": "List an event's routing groups, ordered stations, and assigned teams."
|
|
1175
1175
|
},
|
|
1176
|
+
{
|
|
1177
|
+
"effects": [],
|
|
1178
|
+
"input": "json",
|
|
1179
|
+
"operation": "query",
|
|
1180
|
+
"path": ["routing-group", "release-history"],
|
|
1181
|
+
"procedure": "team.listRoutingGroupReleaseHistory",
|
|
1182
|
+
"requiresAuth": true,
|
|
1183
|
+
"summary": "Show a routing group's five most recent release changes with actor, action and before/after UTC values."
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
"effects": ["state-change"],
|
|
1187
|
+
"input": "json",
|
|
1188
|
+
"operation": "mutation",
|
|
1189
|
+
"path": ["routing-group", "release"],
|
|
1190
|
+
"procedure": "team.updateRoutingGroupRelease",
|
|
1191
|
+
"requiresAuth": true,
|
|
1192
|
+
"summary": "Schedule (future releaseAt ISO timestamp with offset), CLEAR schedule, HOLD, RELEASE_SCHEDULED_HOLD (keep schedule), or RELEASE_NOW for a routing group. After a group starts, only HOLD and RELEASE_SCHEDULED_HOLD are accepted. Requires launchEvent permission.",
|
|
1193
|
+
"targetKeys": ["groupId", "eventId"]
|
|
1194
|
+
},
|
|
1176
1195
|
{
|
|
1177
1196
|
"effects": ["state-change"],
|
|
1178
1197
|
"input": "json",
|
package/command-reference.md
CHANGED
|
@@ -450,6 +450,22 @@ List an event's routing groups, ordered stations, and assigned teams.
|
|
|
450
450
|
- Input: `--input '<json>'` required
|
|
451
451
|
- Effects: read-only
|
|
452
452
|
|
|
453
|
+
## `gosh routing-group release-history`
|
|
454
|
+
|
|
455
|
+
Show a routing group's five most recent release changes with actor, action and before/after UTC values.
|
|
456
|
+
|
|
457
|
+
- Authentication: browser-authenticated CLI session required
|
|
458
|
+
- Input: `--input '<json>'` required
|
|
459
|
+
- Effects: read-only
|
|
460
|
+
|
|
461
|
+
## `gosh routing-group release`
|
|
462
|
+
|
|
463
|
+
Schedule (future releaseAt ISO timestamp with offset), CLEAR schedule, HOLD, RELEASE_SCHEDULED_HOLD (keep schedule), or RELEASE_NOW for a routing group. After a group starts, only HOLD and RELEASE_SCHEDULED_HOLD are accepted. Requires launchEvent permission.
|
|
464
|
+
|
|
465
|
+
- Authentication: browser-authenticated CLI session required
|
|
466
|
+
- Input: `--input '<json>'` required
|
|
467
|
+
- Effects: state-change
|
|
468
|
+
|
|
453
469
|
## `gosh routing-group create`
|
|
454
470
|
|
|
455
471
|
Create a routing group within an event before play starts.
|
package/dist/index.js
CHANGED
|
@@ -35,16 +35,16 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
35
35
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
36
36
|
var __require = import.meta.require;
|
|
37
37
|
|
|
38
|
-
// ../../../node_modules/.bun/dotenv@18.0.
|
|
38
|
+
// ../../../node_modules/.bun/dotenv@18.0.6/node_modules/dotenv/dist/index.cjs
|
|
39
39
|
var require_dist = __commonJS(function(exports, module) {
|
|
40
|
-
var
|
|
40
|
+
var S = (e, o) => () => {
|
|
41
41
|
try {
|
|
42
42
|
return o || e((o = { exports: {} }).exports, o), o.exports;
|
|
43
43
|
} catch (t) {
|
|
44
44
|
throw o = 0, t;
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
-
var
|
|
47
|
+
var j = S((xe, U) => {
|
|
48
48
|
function G(e) {
|
|
49
49
|
return typeof e == "string" ? !["false", "0", "no", "off", ""].includes(e.toLowerCase()) : !!e;
|
|
50
50
|
}
|
|
@@ -58,8 +58,8 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
58
58
|
}
|
|
59
59
|
U.exports = { parseBoolean: G, optionsFromEnv: X };
|
|
60
60
|
});
|
|
61
|
-
var N =
|
|
62
|
-
var Y = __require("fs"),
|
|
61
|
+
var N = S((ye, x) => {
|
|
62
|
+
var Y = __require("fs"), k = __require("path"), z = __require("os"), { URL: Z, fileURLToPath: ee } = __require("url"), { parseBoolean: $, optionsFromEnv: B } = j(), te = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg, b = new Uint8Array(256);
|
|
63
63
|
for (let e = 48;e <= 57; e++)
|
|
64
64
|
b[e] = 1;
|
|
65
65
|
for (let e = 65;e <= 90; e++)
|
|
@@ -185,7 +185,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
185
185
|
return o;
|
|
186
186
|
}
|
|
187
187
|
function ne(e, o) {
|
|
188
|
-
return o &&
|
|
188
|
+
return o && $(o.fast) ? oe(e) : re(e);
|
|
189
189
|
}
|
|
190
190
|
function T(e) {
|
|
191
191
|
console.log(`\u2506 ${e}`);
|
|
@@ -194,16 +194,16 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
194
194
|
console.error(`\u25C7 ${e}`);
|
|
195
195
|
}
|
|
196
196
|
function V(e) {
|
|
197
|
-
return e[0] === "~" ?
|
|
197
|
+
return e[0] === "~" ? k.join(z.homedir(), e.slice(1)) : e;
|
|
198
198
|
}
|
|
199
199
|
function ie(e = {}) {
|
|
200
200
|
return { ...B(), ...e };
|
|
201
201
|
}
|
|
202
202
|
function ce(e) {
|
|
203
203
|
e = ie(e);
|
|
204
|
-
let o =
|
|
204
|
+
let o = k.resolve(process.cwd(), ".env"), t = "utf8", n = process.env;
|
|
205
205
|
e && e.processEnv != null && (n = e.processEnv);
|
|
206
|
-
let r =
|
|
206
|
+
let r = $(e && e.debug);
|
|
207
207
|
e && e.encoding ? t = e.encoding : r && T("no encoding is specified (UTF-8 is used by default)");
|
|
208
208
|
let s = [o];
|
|
209
209
|
if (e && e.path)
|
|
@@ -222,12 +222,12 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
222
222
|
} catch (a) {
|
|
223
223
|
r && T(`failed to load ${c} ${a.message}`), i = a;
|
|
224
224
|
}
|
|
225
|
-
let p = A.populate(n, l, e), f =
|
|
225
|
+
let p = A.populate(n, l, e), f = $(Object.prototype.hasOwnProperty.call(e, "quiet") ? e.quiet : B(n).quiet);
|
|
226
226
|
if (r || !f) {
|
|
227
227
|
let c = Object.keys(p).length, a = [];
|
|
228
228
|
for (let g of s)
|
|
229
229
|
try {
|
|
230
|
-
let h =
|
|
230
|
+
let h = k.relative(process.cwd(), g instanceof Z ? ee(g) : g);
|
|
231
231
|
a.push(h);
|
|
232
232
|
} catch (h) {
|
|
233
233
|
r && T(`failed to load ${g} ${h.message}`), i = h;
|
|
@@ -240,7 +240,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
240
240
|
return A.configDotenv(e);
|
|
241
241
|
}
|
|
242
242
|
function le(e, o, t = {}) {
|
|
243
|
-
let n =
|
|
243
|
+
let n = $(t && t.debug), r = $(t && t.override), s = {};
|
|
244
244
|
if (e === null || typeof e != "object" || o === null || typeof o != "object") {
|
|
245
245
|
let i = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
|
|
246
246
|
throw i.code = "OBJECT_REQUIRED", i;
|
|
@@ -256,7 +256,7 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
256
256
|
x.exports.populate = A.populate;
|
|
257
257
|
x.exports = A;
|
|
258
258
|
});
|
|
259
|
-
var M =
|
|
259
|
+
var M = S((Te, W) => {
|
|
260
260
|
var _ = __require("child_process"), fe = __require("fs"), L = __require("path");
|
|
261
261
|
function ue(e) {
|
|
262
262
|
let o = ['"'], t = 0;
|
|
@@ -309,9 +309,9 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
309
309
|
}
|
|
310
310
|
W.exports = pe;
|
|
311
311
|
});
|
|
312
|
-
var K =
|
|
313
|
-
var he = __require("fs"), ge = __require("os"), Q = __require("path"), me = __require("child_process"), ve = M(), R = N(), { optionsFromEnv: Ce } =
|
|
314
|
-
function
|
|
312
|
+
var K = S(($e, F) => {
|
|
313
|
+
var he = __require("fs"), ge = __require("os"), Q = __require("path"), me = __require("child_process"), ve = M(), R = N(), { optionsFromEnv: Ce } = j();
|
|
314
|
+
function D() {
|
|
315
315
|
console.log(["Usage: dotenv run [--help] [-q|--quiet] [--debug] [--override] [--fast] [-f|--file <paths>] [--] <command> [args...]", "", "Run a command with environment variables from a .env file.", "Place dotenv options before the command; all following arguments go to the command.", "", "Options:", " -f, --file <paths> .env paths, comma-separated or repeated (default: .env)", " -q, --quiet suppress the injected env message", " --debug enable debug logging", " --override override existing environment variables", " --fast use the faster character-scanner parser", "", "Environment variables (DOTENV_CONFIG_* names remain as fallbacks):", " DOTENV_PATH, DOTENV_ENCODING, DOTENV_QUIET,", " DOTENV_DEBUG, DOTENV_OVERRIDE,", " DOTENV_FAST"].join(`
|
|
316
316
|
`));
|
|
317
317
|
}
|
|
@@ -383,24 +383,24 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
383
383
|
function J(e) {
|
|
384
384
|
let o = e[0];
|
|
385
385
|
if (o === "--help" || o === "-h") {
|
|
386
|
-
|
|
386
|
+
D();
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
389
|
if (o !== "run") {
|
|
390
|
-
|
|
390
|
+
D(), process.exitCode = 1;
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
393
|
let t = we(e.slice(1));
|
|
394
394
|
if (t.help) {
|
|
395
|
-
|
|
395
|
+
D();
|
|
396
396
|
return;
|
|
397
397
|
}
|
|
398
398
|
if (t.error) {
|
|
399
|
-
console.error(`dotenv: ${t.error}`),
|
|
399
|
+
console.error(`dotenv: ${t.error}`), D(), process.exitCode = 1;
|
|
400
400
|
return;
|
|
401
401
|
}
|
|
402
402
|
if (t.command.length === 0) {
|
|
403
|
-
|
|
403
|
+
D(), process.exitCode = 1;
|
|
404
404
|
return;
|
|
405
405
|
}
|
|
406
406
|
let n = Ee(t);
|
|
@@ -456,13 +456,13 @@ var require_dist = __commonJS(function(exports, module) {
|
|
|
456
456
|
F.exports = J;
|
|
457
457
|
F === __require.main && J(process.argv.slice(2));
|
|
458
458
|
});
|
|
459
|
-
var
|
|
459
|
+
var I = N();
|
|
460
460
|
var Oe = K();
|
|
461
|
-
module.exports =
|
|
462
|
-
module.exports.config =
|
|
463
|
-
module.exports.configDotenv =
|
|
464
|
-
module.exports.parse =
|
|
465
|
-
module.exports.populate =
|
|
461
|
+
module.exports = I;
|
|
462
|
+
module.exports.config = I.config;
|
|
463
|
+
module.exports.configDotenv = I.configDotenv;
|
|
464
|
+
module.exports.parse = I.parse;
|
|
465
|
+
module.exports.populate = I.populate;
|
|
466
466
|
});
|
|
467
467
|
|
|
468
468
|
// src/load-env.ts
|
|
@@ -7453,6 +7453,14 @@ var typedOperations = {
|
|
|
7453
7453
|
kind: "query",
|
|
7454
7454
|
invoke: (client, input) => client.team.listRoutingGroups.query(input)
|
|
7455
7455
|
},
|
|
7456
|
+
"team.listRoutingGroupReleaseHistory": {
|
|
7457
|
+
kind: "query",
|
|
7458
|
+
invoke: (client, input) => client.team.listRoutingGroupReleaseHistory.query(input)
|
|
7459
|
+
},
|
|
7460
|
+
"team.updateRoutingGroupRelease": {
|
|
7461
|
+
kind: "mutation",
|
|
7462
|
+
invoke: (client, input) => client.team.updateRoutingGroupRelease.mutate(input)
|
|
7463
|
+
},
|
|
7456
7464
|
"team.createRoutingGroup": {
|
|
7457
7465
|
kind: "mutation",
|
|
7458
7466
|
invoke: (client, input) => client.team.createRoutingGroup.mutate(input)
|
|
@@ -7865,6 +7873,7 @@ var defaultMutationTargetKeys = [
|
|
|
7865
7873
|
"name"
|
|
7866
7874
|
];
|
|
7867
7875
|
var mutationTargetKeys = {
|
|
7876
|
+
"team.updateRoutingGroupRelease": ["groupId", "eventId"],
|
|
7868
7877
|
"team.setGameplaySettings": ["eventId"],
|
|
7869
7878
|
"team.createRoutingGroup": ["eventId"],
|
|
7870
7879
|
"team.renameRoutingGroup": ["groupId"],
|
|
@@ -7985,6 +7994,8 @@ var commands = [
|
|
|
7985
7994
|
mutation(["team", "set-station-routes"], "team.setStationRoutes", "Replace a team's linked station route and set its order.", ["state-change"]),
|
|
7986
7995
|
mutation(["event", "set-gameplay"], "team.setGameplaySettings", "Set open or sequential progression, group routing, and per-mission skipping before play starts.", ["state-change"]),
|
|
7987
7996
|
query(["routing-group", "list"], "team.listRoutingGroups", "List an event's routing groups, ordered stations, and assigned teams."),
|
|
7997
|
+
query(["routing-group", "release-history"], "team.listRoutingGroupReleaseHistory", "Show a routing group's five most recent release changes with actor, action and before/after UTC values."),
|
|
7998
|
+
mutation(["routing-group", "release"], "team.updateRoutingGroupRelease", "Schedule (future releaseAt ISO timestamp with offset), CLEAR schedule, HOLD, RELEASE_SCHEDULED_HOLD (keep schedule), or RELEASE_NOW for a routing group. After a group starts, only HOLD and RELEASE_SCHEDULED_HOLD are accepted. Requires launchEvent permission.", ["state-change"]),
|
|
7988
7999
|
mutation(["routing-group", "create"], "team.createRoutingGroup", "Create a routing group within an event before play starts.", ["state-change"]),
|
|
7989
8000
|
mutation(["routing-group", "rename"], "team.renameRoutingGroup", "Rename an event's routing group before play starts.", ["state-change"]),
|
|
7990
8001
|
mutation(["routing-group", "delete"], "team.deleteRoutingGroup", "Delete an unassigned routing group before play starts.", ["state-change", "delete"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/gosh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "GOSH CLI for organizers and AI agents — manage organizations, events, teams, stations, missions, templates, and live operations through the GOSH API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@trpc/client": "^11.19.0",
|
|
22
|
-
"dotenv": "^18.0.
|
|
22
|
+
"dotenv": "^18.0.6",
|
|
23
23
|
"superjson": "^2.2.6"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|