fvtt-world-cli 1.0.0 → 1.1.1
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 +18 -15
- package/bin/fvtt-world-cli.js +12 -0
- package/dist/index.js +2479 -128
- package/docs/README.md +2 -2
- package/docs/architecture.md +43 -11
- package/docs/commands.md +198 -37
- package/docs/compatibility.md +4 -2
- package/docs/getting-started.md +19 -10
- package/docs/protocol.md +114 -23
- package/docs/security.md +155 -22
- package/docs/skill.md +12 -8
- package/package.json +5 -6
- package/skills/foundry-world-editor/SKILL.md +68 -29
- package/skills/foundry-world-editor/SKILL.md.sha256 +1 -1
- package/bin/sync-installed-skill.mjs +0 -89
package/dist/index.js
CHANGED
|
@@ -10,7 +10,19 @@ import { readFileSync as readFileSync2 } from "node:fs";
|
|
|
10
10
|
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
11
11
|
|
|
12
12
|
// ../protocol/src/constants.js
|
|
13
|
-
var PROTOCOL_VERSION = "
|
|
13
|
+
var PROTOCOL_VERSION = "1.1.1";
|
|
14
|
+
var PROTOCOL_COMPONENTS = Object.freeze({
|
|
15
|
+
MODULE: "module",
|
|
16
|
+
CLI_DAEMON: "cli-daemon",
|
|
17
|
+
UNKNOWN: "unknown"
|
|
18
|
+
});
|
|
19
|
+
var PROTOCOL_HANDSHAKES = Object.freeze({
|
|
20
|
+
CLI_DAEMON: "cli-daemon",
|
|
21
|
+
MODULE_DAEMON: "module-daemon",
|
|
22
|
+
COMMAND_REQUEST: "command-request",
|
|
23
|
+
DAEMON_REQUEST: "daemon-request",
|
|
24
|
+
UNKNOWN: "unknown"
|
|
25
|
+
});
|
|
14
26
|
var MODULE_ID = "fvtt-world-cli";
|
|
15
27
|
var DEFAULT_DAEMON_URL = "ws://127.0.0.1:47833";
|
|
16
28
|
var AUTH_FIRST_MESSAGE_TIMEOUT_MS = 5e3;
|
|
@@ -26,6 +38,9 @@ var BRIDGE_TAKEOVER_CLOSE_CODE = 4001;
|
|
|
26
38
|
var BRIDGE_TAKEOVER_CLOSE_REASON = "Bridge session taken over by the same pairing";
|
|
27
39
|
var BRIDGE_RELEASE_CLOSE_CODE = 4002;
|
|
28
40
|
var BRIDGE_RELEASE_CLOSE_REASON = "Bridge released";
|
|
41
|
+
var APPROVAL_AWAIT_PARK_CAP_MS = 25e3;
|
|
42
|
+
var APPROVAL_RESULT_RETENTION_MS = 5 * 60 * 1e3;
|
|
43
|
+
var POLICY_DISCOVERY_TIMEOUT_MS = 1500;
|
|
29
44
|
var CLIENT_ID_MIN_LENGTH = 8;
|
|
30
45
|
var CLIENT_ID_MAX_LENGTH = 64;
|
|
31
46
|
var CLIENT_ID_PATTERN = "^[0-9a-fA-F-]+$";
|
|
@@ -87,6 +102,8 @@ var SCENE_THUMBNAIL_MAX_BYTES = 2 * 1024 * 1024;
|
|
|
87
102
|
var SCENE_THUMBNAIL_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
88
103
|
var TABLE_DRAW_MAX_COUNT = 100;
|
|
89
104
|
var SETTING_VALUE_MAX_BYTES = 256 * 1024;
|
|
105
|
+
var MACRO_EXECUTE_TIMEOUT_DEFAULT_MS = 3e4;
|
|
106
|
+
var MACRO_EXECUTE_TIMEOUT_MAX_MS = 3e5;
|
|
90
107
|
var SEARCH_MODES = Object.freeze(["name", "full"]);
|
|
91
108
|
var SEARCH_SOURCES = Object.freeze(["world", "pack"]);
|
|
92
109
|
var SEARCH_SNIPPET_FIELDS = Object.freeze(["text", "systemText", "name"]);
|
|
@@ -166,6 +183,7 @@ var ERROR_CODES = Object.freeze({
|
|
|
166
183
|
COMPENDIUM_NOT_FOUND: "COMPENDIUM_NOT_FOUND",
|
|
167
184
|
COMPENDIUM_ENTRY_NOT_FOUND: "COMPENDIUM_ENTRY_NOT_FOUND",
|
|
168
185
|
IDEMPOTENCY_KEY_CONFLICT: "IDEMPOTENCY_KEY_CONFLICT",
|
|
186
|
+
IDEMPOTENCY_STORE_FULL: "IDEMPOTENCY_STORE_FULL",
|
|
169
187
|
EFFECT_NOT_FOUND: "EFFECT_NOT_FOUND",
|
|
170
188
|
PLAYLIST_NOT_FOUND: "PLAYLIST_NOT_FOUND",
|
|
171
189
|
PLAYLIST_SOUND_NOT_FOUND: "PLAYLIST_SOUND_NOT_FOUND",
|
|
@@ -186,7 +204,11 @@ var ERROR_CODES = Object.freeze({
|
|
|
186
204
|
SETTING_NOT_FOUND: "SETTING_NOT_FOUND",
|
|
187
205
|
SETTING_READ_FAILED: "SETTING_READ_FAILED",
|
|
188
206
|
SETTING_VALUE_NOT_SERIALIZABLE: "SETTING_VALUE_NOT_SERIALIZABLE",
|
|
207
|
+
SETTING_PROTECTED: "SETTING_PROTECTED",
|
|
208
|
+
SETTING_UNREGISTERED: "SETTING_UNREGISTERED",
|
|
209
|
+
MACRO_TIMEOUT: "MACRO_TIMEOUT",
|
|
189
210
|
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
211
|
+
USER_SELF_PROTECTED: "USER_SELF_PROTECTED",
|
|
190
212
|
SEARCH_INDEX_OVERFLOW: "SEARCH_INDEX_OVERFLOW",
|
|
191
213
|
QUERY_TOO_BROAD: "QUERY_TOO_BROAD",
|
|
192
214
|
UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
|
|
@@ -199,11 +221,22 @@ var ERROR_CODES = Object.freeze({
|
|
|
199
221
|
PAIRING_REQUIRED: "PAIRING_REQUIRED",
|
|
200
222
|
PAIRING_NOT_FOUND: "PAIRING_NOT_FOUND",
|
|
201
223
|
PAIRING_EXPIRED: "PAIRING_EXPIRED",
|
|
202
|
-
BRIDGE_BUSY: "BRIDGE_BUSY"
|
|
224
|
+
BRIDGE_BUSY: "BRIDGE_BUSY",
|
|
225
|
+
COMMAND_DENIED: "COMMAND_DENIED",
|
|
226
|
+
APPROVAL_PENDING: "APPROVAL_PENDING",
|
|
227
|
+
APPROVAL_DENIED: "APPROVAL_DENIED",
|
|
228
|
+
APPROVAL_TIMEOUT: "APPROVAL_TIMEOUT",
|
|
229
|
+
APPROVAL_CANCELLED: "APPROVAL_CANCELLED",
|
|
230
|
+
APPROVAL_QUEUE_FULL: "APPROVAL_QUEUE_FULL",
|
|
231
|
+
APPROVAL_UNKNOWN: "APPROVAL_UNKNOWN"
|
|
203
232
|
});
|
|
204
233
|
|
|
205
234
|
// ../protocol/src/schemas/shared.js
|
|
206
|
-
var cmd = (paramsSchema, { mutation = false } = {}) => ({
|
|
235
|
+
var cmd = (paramsSchema, { mutation = false, discovery = true } = {}) => ({
|
|
236
|
+
mutation,
|
|
237
|
+
...discovery ? {} : { discovery: false },
|
|
238
|
+
paramsSchema
|
|
239
|
+
});
|
|
207
240
|
function mergeCommandFamilies(families) {
|
|
208
241
|
const merged = {};
|
|
209
242
|
for (const family of families) {
|
|
@@ -282,6 +315,9 @@ var tokenIncludeProperty = {
|
|
|
282
315
|
var compendiumIncludeProperty = {
|
|
283
316
|
include: { type: "array", items: { type: "string", enum: ["effects"] } }
|
|
284
317
|
};
|
|
318
|
+
var userIdsProperty = {
|
|
319
|
+
userIds: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 }
|
|
320
|
+
};
|
|
285
321
|
var idempotencyKeyProperty = {
|
|
286
322
|
idempotencyKey: { type: "string", minLength: 1 }
|
|
287
323
|
};
|
|
@@ -494,9 +530,96 @@ var userIdSchema = {
|
|
|
494
530
|
properties: { userId: { type: "string", minLength: 1 } },
|
|
495
531
|
additionalProperties: false
|
|
496
532
|
};
|
|
533
|
+
var userRoleSchema = { type: "integer", enum: [1, 2, 3, 4] };
|
|
534
|
+
var userDataSchema = {
|
|
535
|
+
type: "object",
|
|
536
|
+
required: ["name"],
|
|
537
|
+
properties: {
|
|
538
|
+
name: { type: "string", minLength: 1 },
|
|
539
|
+
role: userRoleSchema,
|
|
540
|
+
color: { type: "string", minLength: 1 },
|
|
541
|
+
pronouns: { type: "string" },
|
|
542
|
+
avatar: nullableStringSchema,
|
|
543
|
+
character: nullableStringSchema,
|
|
544
|
+
flags: freeformObjectSchema
|
|
545
|
+
},
|
|
546
|
+
additionalProperties: false
|
|
547
|
+
};
|
|
548
|
+
var userPatchSchema = patchFrom(userDataSchema, { omit: ["role"] });
|
|
549
|
+
var userPermissionsSchema = {
|
|
550
|
+
type: "object",
|
|
551
|
+
required: [],
|
|
552
|
+
properties: {},
|
|
553
|
+
additionalProperties: { type: ["boolean", "null"] },
|
|
554
|
+
minProperties: 1
|
|
555
|
+
};
|
|
497
556
|
var userCommands = {
|
|
498
557
|
"user.list": cmd(nameFilteredListSchema),
|
|
499
|
-
"user.get": cmd(userIdSchema)
|
|
558
|
+
"user.get": cmd(userIdSchema),
|
|
559
|
+
"user.create": cmd(
|
|
560
|
+
{
|
|
561
|
+
type: "object",
|
|
562
|
+
required: ["data"],
|
|
563
|
+
properties: {
|
|
564
|
+
data: userDataSchema,
|
|
565
|
+
...dryRunProperty,
|
|
566
|
+
...idempotencyKeyProperty
|
|
567
|
+
},
|
|
568
|
+
additionalProperties: false
|
|
569
|
+
},
|
|
570
|
+
{ mutation: true }
|
|
571
|
+
),
|
|
572
|
+
"user.update": cmd(
|
|
573
|
+
{
|
|
574
|
+
type: "object",
|
|
575
|
+
required: ["userId", "patch"],
|
|
576
|
+
properties: {
|
|
577
|
+
userId: { type: "string", minLength: 1 },
|
|
578
|
+
patch: userPatchSchema,
|
|
579
|
+
...dryRunProperty
|
|
580
|
+
},
|
|
581
|
+
additionalProperties: false
|
|
582
|
+
},
|
|
583
|
+
{ mutation: true }
|
|
584
|
+
),
|
|
585
|
+
"user.delete": cmd(
|
|
586
|
+
{
|
|
587
|
+
type: "object",
|
|
588
|
+
required: ["userId"],
|
|
589
|
+
properties: {
|
|
590
|
+
userId: { type: "string", minLength: 1 },
|
|
591
|
+
...dryRunProperty
|
|
592
|
+
},
|
|
593
|
+
additionalProperties: false
|
|
594
|
+
},
|
|
595
|
+
{ mutation: true }
|
|
596
|
+
),
|
|
597
|
+
"user.role.set": cmd(
|
|
598
|
+
{
|
|
599
|
+
type: "object",
|
|
600
|
+
required: ["userId", "role"],
|
|
601
|
+
properties: {
|
|
602
|
+
userId: { type: "string", minLength: 1 },
|
|
603
|
+
role: userRoleSchema,
|
|
604
|
+
...dryRunProperty
|
|
605
|
+
},
|
|
606
|
+
additionalProperties: false
|
|
607
|
+
},
|
|
608
|
+
{ mutation: true }
|
|
609
|
+
),
|
|
610
|
+
"user.permissions.set": cmd(
|
|
611
|
+
{
|
|
612
|
+
type: "object",
|
|
613
|
+
required: ["userId", "permissions"],
|
|
614
|
+
properties: {
|
|
615
|
+
userId: { type: "string", minLength: 1 },
|
|
616
|
+
permissions: userPermissionsSchema,
|
|
617
|
+
...dryRunProperty
|
|
618
|
+
},
|
|
619
|
+
additionalProperties: false
|
|
620
|
+
},
|
|
621
|
+
{ mutation: true }
|
|
622
|
+
)
|
|
500
623
|
};
|
|
501
624
|
|
|
502
625
|
// ../protocol/src/schemas/actor.js
|
|
@@ -873,6 +996,34 @@ var actorCompendiumImportCommands = {
|
|
|
873
996
|
"actor.import-from-compendium": cmd(compendiumImportSchema(actorPatchSchema), { mutation: true })
|
|
874
997
|
};
|
|
875
998
|
|
|
999
|
+
// ../protocol/src/schemas/approval.js
|
|
1000
|
+
var approvalIdProperty = {
|
|
1001
|
+
approvalId: { type: "string", minLength: 22, maxLength: 22, pattern: "^[A-Za-z0-9_-]{22}$" }
|
|
1002
|
+
};
|
|
1003
|
+
var approvalAwaitSchema = {
|
|
1004
|
+
type: "object",
|
|
1005
|
+
required: ["approvalId"],
|
|
1006
|
+
properties: {
|
|
1007
|
+
...approvalIdProperty,
|
|
1008
|
+
waitMs: { type: "integer", minimum: 0, maximum: APPROVAL_AWAIT_PARK_CAP_MS }
|
|
1009
|
+
},
|
|
1010
|
+
additionalProperties: false
|
|
1011
|
+
};
|
|
1012
|
+
var approvalCancelSchema = {
|
|
1013
|
+
type: "object",
|
|
1014
|
+
required: ["approvalId"],
|
|
1015
|
+
properties: { ...approvalIdProperty },
|
|
1016
|
+
additionalProperties: false
|
|
1017
|
+
};
|
|
1018
|
+
var APPROVAL_AWAIT_COMMAND = "approval.await";
|
|
1019
|
+
var APPROVAL_CANCEL_COMMAND = "approval.cancel";
|
|
1020
|
+
var POLICY_SNAPSHOT_COMMAND = "policy.snapshot";
|
|
1021
|
+
var approvalCommands = {
|
|
1022
|
+
[APPROVAL_AWAIT_COMMAND]: cmd(approvalAwaitSchema, { discovery: false }),
|
|
1023
|
+
[APPROVAL_CANCEL_COMMAND]: cmd(approvalCancelSchema, { discovery: false }),
|
|
1024
|
+
[POLICY_SNAPSHOT_COMMAND]: cmd(emptyObjectSchema, { discovery: false })
|
|
1025
|
+
};
|
|
1026
|
+
|
|
876
1027
|
// ../protocol/src/schemas/cards.js
|
|
877
1028
|
var cardBackSchema = {
|
|
878
1029
|
type: "object",
|
|
@@ -1266,6 +1417,15 @@ var chatCommands = {
|
|
|
1266
1417
|
additionalProperties: false
|
|
1267
1418
|
},
|
|
1268
1419
|
{ mutation: true }
|
|
1420
|
+
),
|
|
1421
|
+
"chat.flush": cmd(
|
|
1422
|
+
{
|
|
1423
|
+
type: "object",
|
|
1424
|
+
required: [],
|
|
1425
|
+
properties: { ...dryRunProperty },
|
|
1426
|
+
additionalProperties: false
|
|
1427
|
+
},
|
|
1428
|
+
{ mutation: true }
|
|
1269
1429
|
)
|
|
1270
1430
|
};
|
|
1271
1431
|
|
|
@@ -1810,6 +1970,36 @@ var folderCommands = {
|
|
|
1810
1970
|
"folder.delete": cmd(folderDeleteSchema, { mutation: true })
|
|
1811
1971
|
};
|
|
1812
1972
|
|
|
1973
|
+
// ../protocol/src/schemas/game.js
|
|
1974
|
+
var gameCommands = {
|
|
1975
|
+
"game.pause": cmd(
|
|
1976
|
+
{
|
|
1977
|
+
type: "object",
|
|
1978
|
+
required: ["paused"],
|
|
1979
|
+
properties: {
|
|
1980
|
+
paused: { type: "boolean" },
|
|
1981
|
+
...dryRunProperty
|
|
1982
|
+
},
|
|
1983
|
+
additionalProperties: false
|
|
1984
|
+
},
|
|
1985
|
+
{ mutation: true }
|
|
1986
|
+
)
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
// ../protocol/src/schemas/image.js
|
|
1990
|
+
var imageCommands = {
|
|
1991
|
+
"image.show": cmd({
|
|
1992
|
+
type: "object",
|
|
1993
|
+
required: ["src"],
|
|
1994
|
+
properties: {
|
|
1995
|
+
src: { type: "string", minLength: 1 },
|
|
1996
|
+
title: { type: "string", minLength: 1 },
|
|
1997
|
+
...userIdsProperty
|
|
1998
|
+
},
|
|
1999
|
+
additionalProperties: false
|
|
2000
|
+
})
|
|
2001
|
+
};
|
|
2002
|
+
|
|
1813
2003
|
// ../protocol/src/schemas/item.js
|
|
1814
2004
|
var itemCommands = {
|
|
1815
2005
|
"item.list": cmd(nameFilteredListSchema),
|
|
@@ -2193,6 +2383,16 @@ var journalCommands = {
|
|
|
2193
2383
|
"journal.import-from-compendium": cmd(compendiumImportSchema(journalDocumentPatchSchema), {
|
|
2194
2384
|
mutation: true
|
|
2195
2385
|
}),
|
|
2386
|
+
"journal.show": cmd({
|
|
2387
|
+
type: "object",
|
|
2388
|
+
required: ["journalId"],
|
|
2389
|
+
properties: {
|
|
2390
|
+
journalId: { type: "string", minLength: 1 },
|
|
2391
|
+
force: { type: "boolean" },
|
|
2392
|
+
...userIdsProperty
|
|
2393
|
+
},
|
|
2394
|
+
additionalProperties: false
|
|
2395
|
+
}),
|
|
2196
2396
|
"journal.ownership.set": cmd(
|
|
2197
2397
|
ownershipSetSchema("journalId", {
|
|
2198
2398
|
levelSchema: journalOwnershipLevelSchema,
|
|
@@ -2277,6 +2477,22 @@ var macroDataSchema = {
|
|
|
2277
2477
|
additionalProperties: false
|
|
2278
2478
|
};
|
|
2279
2479
|
var macroPatchSchema = patchFrom(macroDataSchema);
|
|
2480
|
+
var macroExecuteArgsSchema = {
|
|
2481
|
+
...freeformObjectSchema,
|
|
2482
|
+
propertyNames: { pattern: "^[A-Za-z_$][A-Za-z0-9_$]*$" }
|
|
2483
|
+
};
|
|
2484
|
+
var macroExecuteScopeSchema = {
|
|
2485
|
+
type: "object",
|
|
2486
|
+
required: [],
|
|
2487
|
+
properties: {
|
|
2488
|
+
actorId: { type: "string", minLength: 1 },
|
|
2489
|
+
sceneId: { type: "string", minLength: 1 },
|
|
2490
|
+
tokenId: { type: "string", minLength: 1 },
|
|
2491
|
+
args: macroExecuteArgsSchema
|
|
2492
|
+
},
|
|
2493
|
+
additionalProperties: false,
|
|
2494
|
+
minProperties: 1
|
|
2495
|
+
};
|
|
2280
2496
|
var macroIdSchema = {
|
|
2281
2497
|
type: "object",
|
|
2282
2498
|
required: ["macroId"],
|
|
@@ -2346,6 +2562,20 @@ var macroCommands = {
|
|
|
2346
2562
|
},
|
|
2347
2563
|
{ mutation: true }
|
|
2348
2564
|
),
|
|
2565
|
+
"macro.execute": cmd(
|
|
2566
|
+
{
|
|
2567
|
+
type: "object",
|
|
2568
|
+
required: ["macroId"],
|
|
2569
|
+
properties: {
|
|
2570
|
+
macroId: { type: "string", minLength: 1 },
|
|
2571
|
+
scope: macroExecuteScopeSchema,
|
|
2572
|
+
timeoutMs: { type: "integer", minimum: 1, maximum: MACRO_EXECUTE_TIMEOUT_MAX_MS },
|
|
2573
|
+
...dryRunProperty
|
|
2574
|
+
},
|
|
2575
|
+
additionalProperties: false
|
|
2576
|
+
},
|
|
2577
|
+
{ mutation: true }
|
|
2578
|
+
),
|
|
2349
2579
|
"macro.import-from-compendium": cmd(compendiumImportSchema(macroPatchSchema), { mutation: true }),
|
|
2350
2580
|
"macro.ownership.set": cmd(ownershipSetSchema("macroId", { levelSchema: ownershipLevelSchema }), {
|
|
2351
2581
|
mutation: true
|
|
@@ -2902,6 +3132,26 @@ var regionBehaviorPatchSchema = {
|
|
|
2902
3132
|
additionalProperties: true,
|
|
2903
3133
|
minProperties: 1
|
|
2904
3134
|
};
|
|
3135
|
+
var executableBehaviorTypeSchema = { type: "string", enum: ["executeMacro"] };
|
|
3136
|
+
var executableBehaviorCreateSchema = {
|
|
3137
|
+
type: "object",
|
|
3138
|
+
required: ["type"],
|
|
3139
|
+
properties: {
|
|
3140
|
+
type: executableBehaviorTypeSchema,
|
|
3141
|
+
...regionBehaviorWriteProperties
|
|
3142
|
+
},
|
|
3143
|
+
additionalProperties: true
|
|
3144
|
+
};
|
|
3145
|
+
var executableBehaviorPatchSchema = {
|
|
3146
|
+
type: "object",
|
|
3147
|
+
required: [],
|
|
3148
|
+
properties: {
|
|
3149
|
+
type: executableBehaviorTypeSchema,
|
|
3150
|
+
...regionBehaviorWriteProperties
|
|
3151
|
+
},
|
|
3152
|
+
additionalProperties: true,
|
|
3153
|
+
minProperties: 1
|
|
3154
|
+
};
|
|
2905
3155
|
var sceneEmbeddedCommands = {
|
|
2906
3156
|
"scene.token.list": cmd({
|
|
2907
3157
|
type: "object",
|
|
@@ -3969,6 +4219,52 @@ var sceneEmbeddedCommands = {
|
|
|
3969
4219
|
additionalProperties: false
|
|
3970
4220
|
},
|
|
3971
4221
|
{ mutation: true }
|
|
4222
|
+
),
|
|
4223
|
+
"scene.region.behavior.executable.create": cmd(
|
|
4224
|
+
{
|
|
4225
|
+
type: "object",
|
|
4226
|
+
required: ["sceneId", "regionId", "data"],
|
|
4227
|
+
properties: {
|
|
4228
|
+
sceneId: { type: "string", minLength: 1 },
|
|
4229
|
+
regionId: { type: "string", minLength: 1 },
|
|
4230
|
+
data: executableBehaviorCreateSchema,
|
|
4231
|
+
...dryRunProperty,
|
|
4232
|
+
...idempotencyKeyProperty
|
|
4233
|
+
},
|
|
4234
|
+
additionalProperties: false
|
|
4235
|
+
},
|
|
4236
|
+
{ mutation: true }
|
|
4237
|
+
),
|
|
4238
|
+
"scene.region.behavior.executable.update": cmd(
|
|
4239
|
+
{
|
|
4240
|
+
type: "object",
|
|
4241
|
+
required: ["sceneId", "regionId", "behaviorId", "patch"],
|
|
4242
|
+
properties: {
|
|
4243
|
+
sceneId: { type: "string", minLength: 1 },
|
|
4244
|
+
regionId: { type: "string", minLength: 1 },
|
|
4245
|
+
behaviorId: { type: "string", minLength: 1 },
|
|
4246
|
+
patch: executableBehaviorPatchSchema,
|
|
4247
|
+
...dryRunProperty
|
|
4248
|
+
},
|
|
4249
|
+
additionalProperties: false
|
|
4250
|
+
},
|
|
4251
|
+
{ mutation: true }
|
|
4252
|
+
),
|
|
4253
|
+
"scene.region.behavior.executable.clone": cmd(
|
|
4254
|
+
{
|
|
4255
|
+
type: "object",
|
|
4256
|
+
required: ["sceneId", "regionId", "behaviorId"],
|
|
4257
|
+
properties: {
|
|
4258
|
+
sceneId: { type: "string", minLength: 1 },
|
|
4259
|
+
regionId: { type: "string", minLength: 1 },
|
|
4260
|
+
behaviorId: { type: "string", minLength: 1 },
|
|
4261
|
+
patch: executableBehaviorPatchSchema,
|
|
4262
|
+
...dryRunProperty,
|
|
4263
|
+
...idempotencyKeyProperty
|
|
4264
|
+
},
|
|
4265
|
+
additionalProperties: false
|
|
4266
|
+
},
|
|
4267
|
+
{ mutation: true }
|
|
3972
4268
|
)
|
|
3973
4269
|
};
|
|
3974
4270
|
|
|
@@ -4150,6 +4446,27 @@ var sceneCommands = {
|
|
|
4150
4446
|
},
|
|
4151
4447
|
{ mutation: true }
|
|
4152
4448
|
),
|
|
4449
|
+
"scene.activate": cmd(
|
|
4450
|
+
{
|
|
4451
|
+
type: "object",
|
|
4452
|
+
required: ["sceneId"],
|
|
4453
|
+
properties: {
|
|
4454
|
+
sceneId: { type: "string", minLength: 1 },
|
|
4455
|
+
...dryRunProperty
|
|
4456
|
+
},
|
|
4457
|
+
additionalProperties: false
|
|
4458
|
+
},
|
|
4459
|
+
{ mutation: true }
|
|
4460
|
+
),
|
|
4461
|
+
"scene.pull-users": cmd({
|
|
4462
|
+
type: "object",
|
|
4463
|
+
required: ["sceneId"],
|
|
4464
|
+
properties: {
|
|
4465
|
+
sceneId: { type: "string", minLength: 1 },
|
|
4466
|
+
...userIdsProperty
|
|
4467
|
+
},
|
|
4468
|
+
additionalProperties: false
|
|
4469
|
+
}),
|
|
4153
4470
|
"scene.ownership.set": cmd(ownershipSetSchema("sceneId", { levelSchema: ownershipLevelSchema }), {
|
|
4154
4471
|
mutation: true
|
|
4155
4472
|
})
|
|
@@ -4181,24 +4498,70 @@ var settingListSchema = {
|
|
|
4181
4498
|
properties: { ...nameFilterProperty, ...paginationProperties },
|
|
4182
4499
|
additionalProperties: false
|
|
4183
4500
|
};
|
|
4501
|
+
var settingKeyProperties = {
|
|
4502
|
+
namespace: { type: "string", minLength: 1 },
|
|
4503
|
+
key: { type: "string", minLength: 1 }
|
|
4504
|
+
};
|
|
4184
4505
|
var settingGetSchema = {
|
|
4185
4506
|
type: "object",
|
|
4186
4507
|
required: ["namespace", "key"],
|
|
4508
|
+
properties: { ...settingKeyProperties },
|
|
4509
|
+
additionalProperties: false
|
|
4510
|
+
};
|
|
4511
|
+
var settingWriteItemSchema = {
|
|
4512
|
+
type: "object",
|
|
4513
|
+
required: ["namespace", "key", "value"],
|
|
4187
4514
|
properties: {
|
|
4188
|
-
|
|
4189
|
-
|
|
4515
|
+
...settingKeyProperties,
|
|
4516
|
+
value: {}
|
|
4190
4517
|
},
|
|
4191
4518
|
additionalProperties: false
|
|
4192
4519
|
};
|
|
4193
4520
|
var settingCommands = {
|
|
4194
4521
|
"setting.list": cmd(settingListSchema),
|
|
4195
|
-
"setting.get": cmd(settingGetSchema)
|
|
4522
|
+
"setting.get": cmd(settingGetSchema),
|
|
4523
|
+
"setting.get-many": cmd({
|
|
4524
|
+
type: "object",
|
|
4525
|
+
required: ["ids"],
|
|
4526
|
+
properties: { ...batchIdsProperty },
|
|
4527
|
+
additionalProperties: false
|
|
4528
|
+
}),
|
|
4529
|
+
"setting.set": cmd(
|
|
4530
|
+
{
|
|
4531
|
+
type: "object",
|
|
4532
|
+
required: ["namespace", "key", "value"],
|
|
4533
|
+
properties: {
|
|
4534
|
+
...settingWriteItemSchema.properties,
|
|
4535
|
+
...dryRunProperty
|
|
4536
|
+
},
|
|
4537
|
+
additionalProperties: false
|
|
4538
|
+
},
|
|
4539
|
+
{ mutation: true }
|
|
4540
|
+
),
|
|
4541
|
+
"setting.set-many": cmd(
|
|
4542
|
+
{
|
|
4543
|
+
type: "object",
|
|
4544
|
+
required: ["items"],
|
|
4545
|
+
properties: {
|
|
4546
|
+
items: {
|
|
4547
|
+
type: "array",
|
|
4548
|
+
minItems: 1,
|
|
4549
|
+
maxItems: BATCH_WRITE_MAX_ITEMS,
|
|
4550
|
+
items: settingWriteItemSchema
|
|
4551
|
+
},
|
|
4552
|
+
...dryRunProperty
|
|
4553
|
+
},
|
|
4554
|
+
additionalProperties: false
|
|
4555
|
+
},
|
|
4556
|
+
{ mutation: true }
|
|
4557
|
+
)
|
|
4196
4558
|
};
|
|
4197
4559
|
|
|
4198
4560
|
// ../protocol/src/schemas/system.js
|
|
4199
4561
|
var systemCommands = {
|
|
4200
4562
|
"system.ping": cmd(emptyObjectSchema),
|
|
4201
|
-
"system.info": cmd(emptyObjectSchema)
|
|
4563
|
+
"system.info": cmd(emptyObjectSchema),
|
|
4564
|
+
"system.reload": cmd(emptyObjectSchema)
|
|
4202
4565
|
};
|
|
4203
4566
|
|
|
4204
4567
|
// ../protocol/src/schemas/table.js
|
|
@@ -4450,17 +4813,23 @@ var COMMAND_DEFINITIONS = deepFreeze(
|
|
|
4450
4813
|
fileCommands,
|
|
4451
4814
|
compendiumCommands,
|
|
4452
4815
|
folderCommands,
|
|
4816
|
+
gameCommands,
|
|
4817
|
+
imageCommands,
|
|
4453
4818
|
userCommands,
|
|
4454
4819
|
settingCommands,
|
|
4455
4820
|
actorCompendiumImportCommands,
|
|
4456
4821
|
worldAuditCommands,
|
|
4457
|
-
worldSearchCommands
|
|
4822
|
+
worldSearchCommands,
|
|
4823
|
+
approvalCommands
|
|
4458
4824
|
])
|
|
4459
4825
|
);
|
|
4460
4826
|
var COMMAND_NAMES = deepFreeze(Object.keys(COMMAND_DEFINITIONS));
|
|
4461
4827
|
var WRITE_COMMANDS = deepFreeze(
|
|
4462
4828
|
COMMAND_NAMES.filter((command) => COMMAND_DEFINITIONS[command].mutation === true)
|
|
4463
4829
|
);
|
|
4830
|
+
var DISCOVERABLE_COMMAND_NAMES = deepFreeze(
|
|
4831
|
+
COMMAND_NAMES.filter((command) => COMMAND_DEFINITIONS[command].discovery !== false)
|
|
4832
|
+
);
|
|
4464
4833
|
var REQUEST_SCHEMA = {
|
|
4465
4834
|
type: "object",
|
|
4466
4835
|
required: ["protocolVersion", "type", "id", "command", "params"],
|
|
@@ -4808,6 +5177,370 @@ function getInvalidCommandError(command) {
|
|
|
4808
5177
|
};
|
|
4809
5178
|
}
|
|
4810
5179
|
|
|
5180
|
+
// ../protocol/src/generated/default-command-profile.js
|
|
5181
|
+
var DEFAULT_COMMAND_PROFILE = deepFreeze({
|
|
5182
|
+
"system.ping": "allow",
|
|
5183
|
+
"system.info": "allow",
|
|
5184
|
+
"system.reload": "approve",
|
|
5185
|
+
"scene.list": "allow",
|
|
5186
|
+
"scene.get": "allow",
|
|
5187
|
+
"scene.get-many": "allow",
|
|
5188
|
+
"scene.update": "allow",
|
|
5189
|
+
"scene.create": "allow",
|
|
5190
|
+
"scene.delete": "approve",
|
|
5191
|
+
"scene.clone": "allow",
|
|
5192
|
+
"scene.import-from-compendium": "allow",
|
|
5193
|
+
"scene.thumbnail.generate": "allow",
|
|
5194
|
+
"scene.fog.reset": "approve",
|
|
5195
|
+
"scene.activate": "allow",
|
|
5196
|
+
"scene.pull-users": "allow",
|
|
5197
|
+
"scene.ownership.set": "allow",
|
|
5198
|
+
"scene.token.list": "allow",
|
|
5199
|
+
"scene.token.get": "allow",
|
|
5200
|
+
"scene.token.create": "allow",
|
|
5201
|
+
"scene.token.update": "allow",
|
|
5202
|
+
"scene.token.delete": "approve",
|
|
5203
|
+
"scene.token.clone": "allow",
|
|
5204
|
+
"scene.token.create-many": "allow",
|
|
5205
|
+
"scene.token.update-many": "allow",
|
|
5206
|
+
"scene.token.delete-many": "approve",
|
|
5207
|
+
"scene.token.item.list": "allow",
|
|
5208
|
+
"scene.token.item.get": "allow",
|
|
5209
|
+
"scene.token.item.create": "allow",
|
|
5210
|
+
"scene.token.item.update": "allow",
|
|
5211
|
+
"scene.token.item.delete": "approve",
|
|
5212
|
+
"scene.token.item.clone": "allow",
|
|
5213
|
+
"scene.token.item.effect.list": "allow",
|
|
5214
|
+
"scene.token.item.effect.get": "allow",
|
|
5215
|
+
"scene.token.item.effect.create": "allow",
|
|
5216
|
+
"scene.token.item.effect.update": "allow",
|
|
5217
|
+
"scene.token.item.effect.delete": "approve",
|
|
5218
|
+
"scene.token.item.effect.clone": "allow",
|
|
5219
|
+
"scene.token.item.effect.create-many": "allow",
|
|
5220
|
+
"scene.token.item.effect.update-many": "allow",
|
|
5221
|
+
"scene.token.item.effect.delete-many": "approve",
|
|
5222
|
+
"scene.token.effect.list": "allow",
|
|
5223
|
+
"scene.token.effect.applied": "allow",
|
|
5224
|
+
"scene.token.effect.get": "allow",
|
|
5225
|
+
"scene.token.effect.create": "allow",
|
|
5226
|
+
"scene.token.effect.update": "allow",
|
|
5227
|
+
"scene.token.effect.delete": "approve",
|
|
5228
|
+
"scene.token.effect.clone": "allow",
|
|
5229
|
+
"scene.token.effect.create-many": "allow",
|
|
5230
|
+
"scene.token.effect.update-many": "allow",
|
|
5231
|
+
"scene.token.effect.delete-many": "approve",
|
|
5232
|
+
"scene.tile.list": "allow",
|
|
5233
|
+
"scene.tile.get": "allow",
|
|
5234
|
+
"scene.tile.create": "allow",
|
|
5235
|
+
"scene.tile.update": "allow",
|
|
5236
|
+
"scene.tile.delete": "approve",
|
|
5237
|
+
"scene.tile.clone": "allow",
|
|
5238
|
+
"scene.tile.create-many": "allow",
|
|
5239
|
+
"scene.tile.update-many": "allow",
|
|
5240
|
+
"scene.tile.delete-many": "approve",
|
|
5241
|
+
"scene.sound.list": "allow",
|
|
5242
|
+
"scene.sound.get": "allow",
|
|
5243
|
+
"scene.sound.create": "allow",
|
|
5244
|
+
"scene.sound.update": "allow",
|
|
5245
|
+
"scene.sound.delete": "approve",
|
|
5246
|
+
"scene.sound.clone": "allow",
|
|
5247
|
+
"scene.sound.create-many": "allow",
|
|
5248
|
+
"scene.sound.update-many": "allow",
|
|
5249
|
+
"scene.sound.delete-many": "approve",
|
|
5250
|
+
"scene.wall.list": "allow",
|
|
5251
|
+
"scene.wall.get": "allow",
|
|
5252
|
+
"scene.wall.create": "allow",
|
|
5253
|
+
"scene.wall.update": "allow",
|
|
5254
|
+
"scene.wall.delete": "approve",
|
|
5255
|
+
"scene.wall.clone": "allow",
|
|
5256
|
+
"scene.wall.create-many": "allow",
|
|
5257
|
+
"scene.wall.update-many": "allow",
|
|
5258
|
+
"scene.wall.delete-many": "approve",
|
|
5259
|
+
"scene.note.list": "allow",
|
|
5260
|
+
"scene.note.get": "allow",
|
|
5261
|
+
"scene.note.create": "allow",
|
|
5262
|
+
"scene.note.update": "allow",
|
|
5263
|
+
"scene.note.delete": "approve",
|
|
5264
|
+
"scene.note.clone": "allow",
|
|
5265
|
+
"scene.note.create-many": "allow",
|
|
5266
|
+
"scene.note.update-many": "allow",
|
|
5267
|
+
"scene.note.delete-many": "approve",
|
|
5268
|
+
"scene.drawing.list": "allow",
|
|
5269
|
+
"scene.drawing.get": "allow",
|
|
5270
|
+
"scene.drawing.create": "allow",
|
|
5271
|
+
"scene.drawing.update": "allow",
|
|
5272
|
+
"scene.drawing.delete": "approve",
|
|
5273
|
+
"scene.drawing.clone": "allow",
|
|
5274
|
+
"scene.drawing.create-many": "allow",
|
|
5275
|
+
"scene.drawing.update-many": "allow",
|
|
5276
|
+
"scene.drawing.delete-many": "approve",
|
|
5277
|
+
"scene.light.list": "allow",
|
|
5278
|
+
"scene.light.get": "allow",
|
|
5279
|
+
"scene.light.create": "allow",
|
|
5280
|
+
"scene.light.update": "allow",
|
|
5281
|
+
"scene.light.delete": "approve",
|
|
5282
|
+
"scene.light.clone": "allow",
|
|
5283
|
+
"scene.light.create-many": "allow",
|
|
5284
|
+
"scene.light.update-many": "allow",
|
|
5285
|
+
"scene.light.delete-many": "approve",
|
|
5286
|
+
"scene.template.list": "allow",
|
|
5287
|
+
"scene.template.get": "allow",
|
|
5288
|
+
"scene.template.create": "allow",
|
|
5289
|
+
"scene.template.update": "allow",
|
|
5290
|
+
"scene.template.delete": "approve",
|
|
5291
|
+
"scene.template.clone": "allow",
|
|
5292
|
+
"scene.template.create-many": "allow",
|
|
5293
|
+
"scene.template.update-many": "allow",
|
|
5294
|
+
"scene.template.delete-many": "approve",
|
|
5295
|
+
"scene.region.list": "allow",
|
|
5296
|
+
"scene.region.get": "allow",
|
|
5297
|
+
"scene.region.create": "allow",
|
|
5298
|
+
"scene.region.update": "allow",
|
|
5299
|
+
"scene.region.delete": "approve",
|
|
5300
|
+
"scene.region.clone": "allow",
|
|
5301
|
+
"scene.region.create-many": "allow",
|
|
5302
|
+
"scene.region.update-many": "allow",
|
|
5303
|
+
"scene.region.delete-many": "approve",
|
|
5304
|
+
"scene.region.behavior.list": "allow",
|
|
5305
|
+
"scene.region.behavior.get": "allow",
|
|
5306
|
+
"scene.region.behavior.create": "allow",
|
|
5307
|
+
"scene.region.behavior.update": "allow",
|
|
5308
|
+
"scene.region.behavior.delete": "approve",
|
|
5309
|
+
"scene.region.behavior.clone": "allow",
|
|
5310
|
+
"scene.region.behavior.executable.create": "deny",
|
|
5311
|
+
"scene.region.behavior.executable.update": "deny",
|
|
5312
|
+
"scene.region.behavior.executable.clone": "deny",
|
|
5313
|
+
"item.list": "allow",
|
|
5314
|
+
"item.get": "allow",
|
|
5315
|
+
"item.get-many": "allow",
|
|
5316
|
+
"item.create": "allow",
|
|
5317
|
+
"item.update": "allow",
|
|
5318
|
+
"item.delete": "approve",
|
|
5319
|
+
"item.clone": "allow",
|
|
5320
|
+
"item.import-from-compendium": "allow",
|
|
5321
|
+
"item.ownership.set": "allow",
|
|
5322
|
+
"item.update-many": "allow",
|
|
5323
|
+
"item.delete-many": "approve",
|
|
5324
|
+
"item.effect.list": "allow",
|
|
5325
|
+
"item.effect.get": "allow",
|
|
5326
|
+
"item.effect.create": "allow",
|
|
5327
|
+
"item.effect.update": "allow",
|
|
5328
|
+
"item.effect.delete": "approve",
|
|
5329
|
+
"item.effect.clone": "allow",
|
|
5330
|
+
"item.effect.create-many": "allow",
|
|
5331
|
+
"item.effect.update-many": "allow",
|
|
5332
|
+
"item.effect.delete-many": "approve",
|
|
5333
|
+
"journal.list": "allow",
|
|
5334
|
+
"journal.get": "allow",
|
|
5335
|
+
"journal.get-many": "allow",
|
|
5336
|
+
"journal.create": "allow",
|
|
5337
|
+
"journal.update": "allow",
|
|
5338
|
+
"journal.delete": "approve",
|
|
5339
|
+
"journal.clone": "allow",
|
|
5340
|
+
"journal.import-from-compendium": "allow",
|
|
5341
|
+
"journal.show": "allow",
|
|
5342
|
+
"journal.ownership.set": "allow",
|
|
5343
|
+
"journal.update-many": "allow",
|
|
5344
|
+
"journal.delete-many": "approve",
|
|
5345
|
+
"journal.category.list": "allow",
|
|
5346
|
+
"journal.category.get": "allow",
|
|
5347
|
+
"journal.category.create": "allow",
|
|
5348
|
+
"journal.category.update": "allow",
|
|
5349
|
+
"journal.category.delete": "approve",
|
|
5350
|
+
"macro.list": "allow",
|
|
5351
|
+
"macro.get": "allow",
|
|
5352
|
+
"macro.get-many": "allow",
|
|
5353
|
+
"macro.create": "allow",
|
|
5354
|
+
"macro.update": "allow",
|
|
5355
|
+
"macro.delete": "approve",
|
|
5356
|
+
"macro.clone": "allow",
|
|
5357
|
+
"macro.execute": "deny",
|
|
5358
|
+
"macro.import-from-compendium": "allow",
|
|
5359
|
+
"macro.ownership.set": "allow",
|
|
5360
|
+
"playlist.list": "allow",
|
|
5361
|
+
"playlist.get": "allow",
|
|
5362
|
+
"playlist.get-many": "allow",
|
|
5363
|
+
"playlist.create": "allow",
|
|
5364
|
+
"playlist.update": "allow",
|
|
5365
|
+
"playlist.delete": "approve",
|
|
5366
|
+
"playlist.clone": "allow",
|
|
5367
|
+
"playlist.import-from-compendium": "allow",
|
|
5368
|
+
"playlist.ownership.set": "allow",
|
|
5369
|
+
"playlist.sound.list": "allow",
|
|
5370
|
+
"playlist.sound.get": "allow",
|
|
5371
|
+
"playlist.sound.create": "allow",
|
|
5372
|
+
"playlist.sound.update": "allow",
|
|
5373
|
+
"playlist.sound.delete": "approve",
|
|
5374
|
+
"playlist.sound.clone": "allow",
|
|
5375
|
+
"playlist.play": "allow",
|
|
5376
|
+
"playlist.stop": "allow",
|
|
5377
|
+
"playlist.playNext": "allow",
|
|
5378
|
+
"playlist.sound.play": "allow",
|
|
5379
|
+
"playlist.sound.stop": "allow",
|
|
5380
|
+
"table.list": "allow",
|
|
5381
|
+
"table.get": "allow",
|
|
5382
|
+
"table.get-many": "allow",
|
|
5383
|
+
"table.create": "allow",
|
|
5384
|
+
"table.update": "allow",
|
|
5385
|
+
"table.delete": "approve",
|
|
5386
|
+
"table.clone": "allow",
|
|
5387
|
+
"table.import-from-compendium": "allow",
|
|
5388
|
+
"table.draw": "allow",
|
|
5389
|
+
"table.reset": "allow",
|
|
5390
|
+
"table.ownership.set": "allow",
|
|
5391
|
+
"table.result.list": "allow",
|
|
5392
|
+
"table.result.get": "allow",
|
|
5393
|
+
"table.result.create": "allow",
|
|
5394
|
+
"table.result.update": "allow",
|
|
5395
|
+
"table.result.delete": "approve",
|
|
5396
|
+
"table.result.clone": "allow",
|
|
5397
|
+
"combat.list": "allow",
|
|
5398
|
+
"combat.get": "allow",
|
|
5399
|
+
"combat.create": "allow",
|
|
5400
|
+
"combat.update": "allow",
|
|
5401
|
+
"combat.delete": "approve",
|
|
5402
|
+
"combat.start": "allow",
|
|
5403
|
+
"combat.activate": "allow",
|
|
5404
|
+
"combat.next-turn": "allow",
|
|
5405
|
+
"combat.previous-turn": "allow",
|
|
5406
|
+
"combat.next-round": "allow",
|
|
5407
|
+
"combat.previous-round": "allow",
|
|
5408
|
+
"combat.reset-initiative": "allow",
|
|
5409
|
+
"combat.roll-initiative": "allow",
|
|
5410
|
+
"combat.set-initiative": "allow",
|
|
5411
|
+
"combat.combatant.list": "allow",
|
|
5412
|
+
"combat.combatant.get": "allow",
|
|
5413
|
+
"combat.combatant.create": "allow",
|
|
5414
|
+
"combat.combatant.update": "allow",
|
|
5415
|
+
"combat.combatant.delete": "approve",
|
|
5416
|
+
"combat.group.list": "allow",
|
|
5417
|
+
"combat.group.get": "allow",
|
|
5418
|
+
"combat.group.create": "allow",
|
|
5419
|
+
"combat.group.update": "allow",
|
|
5420
|
+
"combat.group.delete": "approve",
|
|
5421
|
+
"cards.list": "allow",
|
|
5422
|
+
"cards.get": "allow",
|
|
5423
|
+
"cards.get-many": "allow",
|
|
5424
|
+
"cards.create": "allow",
|
|
5425
|
+
"cards.update": "allow",
|
|
5426
|
+
"cards.clone": "allow",
|
|
5427
|
+
"cards.import-from-compendium": "allow",
|
|
5428
|
+
"cards.delete": "approve",
|
|
5429
|
+
"cards.ownership.set": "allow",
|
|
5430
|
+
"cards.shuffle": "allow",
|
|
5431
|
+
"cards.reset": "allow",
|
|
5432
|
+
"cards.deal": "allow",
|
|
5433
|
+
"cards.draw": "allow",
|
|
5434
|
+
"cards.pass": "allow",
|
|
5435
|
+
"cards.card.list": "allow",
|
|
5436
|
+
"cards.card.get": "allow",
|
|
5437
|
+
"cards.card.create": "allow",
|
|
5438
|
+
"cards.card.update": "allow",
|
|
5439
|
+
"cards.card.clone": "allow",
|
|
5440
|
+
"cards.card.delete": "approve",
|
|
5441
|
+
"chat.list": "allow",
|
|
5442
|
+
"chat.get": "allow",
|
|
5443
|
+
"chat.create": "allow",
|
|
5444
|
+
"chat.delete": "approve",
|
|
5445
|
+
"chat.flush": "approve",
|
|
5446
|
+
"actor.list": "allow",
|
|
5447
|
+
"actor.get": "allow",
|
|
5448
|
+
"actor.get-many": "allow",
|
|
5449
|
+
"actor.create": "allow",
|
|
5450
|
+
"actor.update": "allow",
|
|
5451
|
+
"actor.delete": "approve",
|
|
5452
|
+
"actor.clone": "allow",
|
|
5453
|
+
"actor.ownership.set": "allow",
|
|
5454
|
+
"actor.item.list": "allow",
|
|
5455
|
+
"actor.item.create": "allow",
|
|
5456
|
+
"actor.item.update": "allow",
|
|
5457
|
+
"actor.item.get": "allow",
|
|
5458
|
+
"actor.item.delete": "approve",
|
|
5459
|
+
"actor.item.clone": "allow",
|
|
5460
|
+
"actor.item.import-from-compendium": "allow",
|
|
5461
|
+
"actor.update-many": "allow",
|
|
5462
|
+
"actor.delete-many": "approve",
|
|
5463
|
+
"actor.effect.list": "allow",
|
|
5464
|
+
"actor.effect.applied": "allow",
|
|
5465
|
+
"actor.effect.get": "allow",
|
|
5466
|
+
"actor.effect.create": "allow",
|
|
5467
|
+
"actor.effect.update": "allow",
|
|
5468
|
+
"actor.effect.delete": "approve",
|
|
5469
|
+
"actor.effect.clone": "allow",
|
|
5470
|
+
"actor.effect.create-many": "allow",
|
|
5471
|
+
"actor.effect.update-many": "allow",
|
|
5472
|
+
"actor.effect.delete-many": "approve",
|
|
5473
|
+
"actor.item.effect.list": "allow",
|
|
5474
|
+
"actor.item.effect.get": "allow",
|
|
5475
|
+
"actor.item.effect.create": "allow",
|
|
5476
|
+
"actor.item.effect.update": "allow",
|
|
5477
|
+
"actor.item.effect.delete": "approve",
|
|
5478
|
+
"actor.item.effect.clone": "allow",
|
|
5479
|
+
"actor.item.effect.create-many": "allow",
|
|
5480
|
+
"actor.item.effect.update-many": "allow",
|
|
5481
|
+
"actor.item.effect.delete-many": "approve",
|
|
5482
|
+
"file.list": "allow",
|
|
5483
|
+
"file.stat": "allow",
|
|
5484
|
+
"file.read": "allow",
|
|
5485
|
+
"file.mkdir": "allow",
|
|
5486
|
+
"file.upload": "allow",
|
|
5487
|
+
"file.delete": "approve",
|
|
5488
|
+
"file.move": "approve",
|
|
5489
|
+
"compendium.list": "allow",
|
|
5490
|
+
"compendium.index": "allow",
|
|
5491
|
+
"compendium.get": "allow",
|
|
5492
|
+
"folder.list": "allow",
|
|
5493
|
+
"folder.get": "allow",
|
|
5494
|
+
"folder.create": "allow",
|
|
5495
|
+
"folder.update": "allow",
|
|
5496
|
+
"folder.delete": "approve",
|
|
5497
|
+
"game.pause": "allow",
|
|
5498
|
+
"image.show": "allow",
|
|
5499
|
+
"user.list": "allow",
|
|
5500
|
+
"user.get": "allow",
|
|
5501
|
+
"user.create": "approve",
|
|
5502
|
+
"user.update": "allow",
|
|
5503
|
+
"user.delete": "approve",
|
|
5504
|
+
"user.role.set": "deny",
|
|
5505
|
+
"user.permissions.set": "deny",
|
|
5506
|
+
"setting.list": "allow",
|
|
5507
|
+
"setting.get": "allow",
|
|
5508
|
+
"setting.get-many": "allow",
|
|
5509
|
+
"setting.set": "deny",
|
|
5510
|
+
"setting.set-many": "deny",
|
|
5511
|
+
"actor.import-from-compendium": "allow",
|
|
5512
|
+
"world.audit-files": "allow",
|
|
5513
|
+
"world.search": "allow",
|
|
5514
|
+
"approval.await": "allow",
|
|
5515
|
+
"approval.cancel": "allow",
|
|
5516
|
+
"policy.snapshot": "allow"
|
|
5517
|
+
});
|
|
5518
|
+
|
|
5519
|
+
// ../protocol/src/command-risk.js
|
|
5520
|
+
var DESTRUCTIVE_VERBS = Object.freeze(["delete", "delete-many"]);
|
|
5521
|
+
var DESTRUCTIVE_COMMANDS = Object.freeze(["chat.flush", "file.move", "scene.fog.reset"]);
|
|
5522
|
+
var APPROVE_EXTRA_COMMANDS = Object.freeze(["system.reload", "user.create"]);
|
|
5523
|
+
var DENIED_BY_DEFAULT_COMMANDS = Object.freeze([
|
|
5524
|
+
"macro.execute",
|
|
5525
|
+
"setting.set",
|
|
5526
|
+
"setting.set-many",
|
|
5527
|
+
"user.role.set",
|
|
5528
|
+
"user.permissions.set",
|
|
5529
|
+
"scene.region.behavior.executable.create",
|
|
5530
|
+
"scene.region.behavior.executable.update",
|
|
5531
|
+
"scene.region.behavior.executable.clone"
|
|
5532
|
+
]);
|
|
5533
|
+
|
|
5534
|
+
// ../protocol/src/policy.js
|
|
5535
|
+
var POLICY_BEHAVIORS = Object.freeze(["allow", "approve", "deny"]);
|
|
5536
|
+
var POLICY_EXEMPT_COMMANDS = Object.freeze([
|
|
5537
|
+
"system.ping",
|
|
5538
|
+
"system.info",
|
|
5539
|
+
"approval.await",
|
|
5540
|
+
"approval.cancel",
|
|
5541
|
+
"policy.snapshot"
|
|
5542
|
+
]);
|
|
5543
|
+
|
|
4811
5544
|
// ../protocol/src/validation.js
|
|
4812
5545
|
function isPlainObject(value) {
|
|
4813
5546
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -4906,6 +5639,18 @@ function validateObjectSchema(schema, value, path, errors) {
|
|
|
4906
5639
|
if (typeof schema.minProperties === "number" && ownKeys.length < schema.minProperties) {
|
|
4907
5640
|
errors.push(`${path} must contain at least ${schema.minProperties} properties`);
|
|
4908
5641
|
}
|
|
5642
|
+
if (isPlainObject(schema.propertyNames) && typeof schema.propertyNames.pattern === "string") {
|
|
5643
|
+
const namePattern = compilePattern(schema.propertyNames.pattern);
|
|
5644
|
+
for (const key of ownKeys) {
|
|
5645
|
+
if (namePattern === null) {
|
|
5646
|
+
errors.push(
|
|
5647
|
+
`${path}.${key} cannot be validated: ${schema.propertyNames.pattern} is not a valid unicode-mode pattern`
|
|
5648
|
+
);
|
|
5649
|
+
} else if (!namePattern.test(key)) {
|
|
5650
|
+
errors.push(`${path}.${key} is not an allowed property name`);
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
4909
5654
|
for (const [key, propertyValue] of Object.entries(value)) {
|
|
4910
5655
|
const propertySchema = properties[key];
|
|
4911
5656
|
if (propertySchema) {
|
|
@@ -5106,13 +5851,68 @@ function createProtocolError({ code, message, details = {} }) {
|
|
|
5106
5851
|
details
|
|
5107
5852
|
};
|
|
5108
5853
|
}
|
|
5109
|
-
|
|
5854
|
+
var LEGACY_PROTOCOL_RELEASES = Object.freeze({ "3.0": "1.0.0" });
|
|
5855
|
+
var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
5856
|
+
function normalizeComparableProtocolVersion(value) {
|
|
5857
|
+
if (typeof value !== "string") {
|
|
5858
|
+
return null;
|
|
5859
|
+
}
|
|
5860
|
+
if (Object.hasOwn(LEGACY_PROTOCOL_RELEASES, value)) {
|
|
5861
|
+
return LEGACY_PROTOCOL_RELEASES[value];
|
|
5862
|
+
}
|
|
5863
|
+
return RELEASE_VERSION_PATTERN.test(value) ? value : null;
|
|
5864
|
+
}
|
|
5865
|
+
function compareReleaseVersions(left, right) {
|
|
5866
|
+
const leftParts = left.split(".").map(Number);
|
|
5867
|
+
const rightParts = right.split(".").map(Number);
|
|
5868
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
5869
|
+
if (leftParts[index] !== rightParts[index]) {
|
|
5870
|
+
return leftParts[index] < rightParts[index] ? -1 : 1;
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
return 0;
|
|
5874
|
+
}
|
|
5875
|
+
function isNamedComponent(component) {
|
|
5876
|
+
return component === PROTOCOL_COMPONENTS.MODULE || component === PROTOCOL_COMPONENTS.CLI_DAEMON;
|
|
5877
|
+
}
|
|
5878
|
+
function resolveStaleness(actualVersion, peer, reporter) {
|
|
5879
|
+
const actual = normalizeComparableProtocolVersion(actualVersion);
|
|
5880
|
+
const expected = normalizeComparableProtocolVersion(PROTOCOL_VERSION);
|
|
5881
|
+
if (actual === null || expected === null) {
|
|
5882
|
+
return { staleComponent: PROTOCOL_COMPONENTS.UNKNOWN, comparable: false };
|
|
5883
|
+
}
|
|
5884
|
+
const order = compareReleaseVersions(actual, expected);
|
|
5885
|
+
const older = order < 0 ? peer : order > 0 ? reporter : PROTOCOL_COMPONENTS.UNKNOWN;
|
|
5886
|
+
return {
|
|
5887
|
+
staleComponent: isNamedComponent(older) ? older : PROTOCOL_COMPONENTS.UNKNOWN,
|
|
5888
|
+
comparable: true
|
|
5889
|
+
};
|
|
5890
|
+
}
|
|
5891
|
+
function describeStaleness({ staleComponent, comparable }) {
|
|
5892
|
+
if (staleComponent === PROTOCOL_COMPONENTS.MODULE) {
|
|
5893
|
+
return "the Foundry module is the older component, so update the module in Foundry until both halves come from the same release, then reload the GM client";
|
|
5894
|
+
}
|
|
5895
|
+
if (staleComponent === PROTOCOL_COMPONENTS.CLI_DAEMON) {
|
|
5896
|
+
return "the CLI and daemon are the older component, so update the fvtt-world-cli package until both halves come from the same release, then restart the daemon";
|
|
5897
|
+
}
|
|
5898
|
+
const cause = comparable ? "the older component is not identified here" : "these versions cannot be ordered, so the older component is unknown";
|
|
5899
|
+
return `${cause}: bring the fvtt-world-cli package and the Foundry module to the same release, restart the daemon, and reload the GM client`;
|
|
5900
|
+
}
|
|
5901
|
+
function getProtocolVersionError(actualVersion, options = {}) {
|
|
5902
|
+
const {
|
|
5903
|
+
peer = PROTOCOL_COMPONENTS.UNKNOWN,
|
|
5904
|
+
reporter = PROTOCOL_COMPONENTS.UNKNOWN,
|
|
5905
|
+
handshake = PROTOCOL_HANDSHAKES.UNKNOWN
|
|
5906
|
+
} = options;
|
|
5907
|
+
const staleness = resolveStaleness(actualVersion, peer, reporter);
|
|
5110
5908
|
return createProtocolError({
|
|
5111
5909
|
code: ERROR_CODES.UNSUPPORTED_PROTOCOL_VERSION,
|
|
5112
|
-
message: `Unsupported protocol version: ${actualVersion}
|
|
5910
|
+
message: `Unsupported protocol version: ${actualVersion} (expected ${PROTOCOL_VERSION}); ${describeStaleness(staleness)}. Components from different releases are refused by design.`,
|
|
5113
5911
|
details: {
|
|
5114
5912
|
expectedVersion: PROTOCOL_VERSION,
|
|
5115
|
-
actualVersion
|
|
5913
|
+
actualVersion,
|
|
5914
|
+
staleComponent: staleness.staleComponent,
|
|
5915
|
+
handshake
|
|
5116
5916
|
}
|
|
5117
5917
|
});
|
|
5118
5918
|
}
|
|
@@ -5345,6 +6145,26 @@ function resolveBridgeServeConfig({
|
|
|
5345
6145
|
}
|
|
5346
6146
|
|
|
5347
6147
|
// src/transport-util.ts
|
|
6148
|
+
var APPROVAL_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
6149
|
+
var MAX_EPOCH_MS = 864e13;
|
|
6150
|
+
function readApprovalId(value) {
|
|
6151
|
+
return typeof value === "string" && APPROVAL_ID_PATTERN.test(value) ? value : null;
|
|
6152
|
+
}
|
|
6153
|
+
function readPendingApprovalDetails(envelope) {
|
|
6154
|
+
if (envelope.ok || envelope.error?.code !== ERROR_CODES.APPROVAL_PENDING) {
|
|
6155
|
+
return null;
|
|
6156
|
+
}
|
|
6157
|
+
const details = envelope.error.details ?? {};
|
|
6158
|
+
const approvalId = readApprovalId(details.approvalId);
|
|
6159
|
+
const expiresAt = details.expiresAt;
|
|
6160
|
+
if (approvalId === null || typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
|
|
6161
|
+
return null;
|
|
6162
|
+
}
|
|
6163
|
+
if (Math.abs(expiresAt) > MAX_EPOCH_MS) {
|
|
6164
|
+
return null;
|
|
6165
|
+
}
|
|
6166
|
+
return { approvalId, expiresAt };
|
|
6167
|
+
}
|
|
5348
6168
|
var DaemonTransportError = class extends Error {
|
|
5349
6169
|
code;
|
|
5350
6170
|
details;
|
|
@@ -5370,6 +6190,21 @@ function normalizeIncomingData(data) {
|
|
|
5370
6190
|
function isCommandResponseEnvelope(message, acceptedTypes) {
|
|
5371
6191
|
return typeof message === "object" && message !== null && acceptedTypes.includes(message.type) && typeof message.id === "string" && typeof message.ok === "boolean";
|
|
5372
6192
|
}
|
|
6193
|
+
function protocolVersionSkewError(message) {
|
|
6194
|
+
if (typeof message !== "object" || message === null) {
|
|
6195
|
+
return null;
|
|
6196
|
+
}
|
|
6197
|
+
const frame = message;
|
|
6198
|
+
if (frame.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK || typeof frame.protocolVersion !== "string" || frame.protocolVersion === PROTOCOL_VERSION) {
|
|
6199
|
+
return null;
|
|
6200
|
+
}
|
|
6201
|
+
const error = getProtocolVersionError(frame.protocolVersion, {
|
|
6202
|
+
peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
|
|
6203
|
+
reporter: PROTOCOL_COMPONENTS.CLI_DAEMON,
|
|
6204
|
+
handshake: PROTOCOL_HANDSHAKES.CLI_DAEMON
|
|
6205
|
+
});
|
|
6206
|
+
return new DaemonTransportError(error.code, error.message, error.details);
|
|
6207
|
+
}
|
|
5373
6208
|
function decodeTransportFrame(data) {
|
|
5374
6209
|
const parsed = parseBridgeMessage(normalizeIncomingData(data));
|
|
5375
6210
|
if (!parsed.ok) {
|
|
@@ -5382,6 +6217,10 @@ function decodeTransportFrame(data) {
|
|
|
5382
6217
|
)
|
|
5383
6218
|
};
|
|
5384
6219
|
}
|
|
6220
|
+
const skew = protocolVersionSkewError(parsed.value);
|
|
6221
|
+
if (skew) {
|
|
6222
|
+
return { ok: false, error: skew };
|
|
6223
|
+
}
|
|
5385
6224
|
const validation = validateTransportMessage(parsed.value);
|
|
5386
6225
|
if (!validation.ok) {
|
|
5387
6226
|
return {
|
|
@@ -5402,10 +6241,195 @@ function sendJson(socket, payload) {
|
|
|
5402
6241
|
socket.send(JSON.stringify(payload));
|
|
5403
6242
|
}
|
|
5404
6243
|
|
|
6244
|
+
// src/bridge/approval-link.ts
|
|
6245
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
6246
|
+
function settlementForAwait(envelope) {
|
|
6247
|
+
if (!envelope.ok) {
|
|
6248
|
+
const approvalId2 = readApprovalId(envelope.error?.details?.approvalId);
|
|
6249
|
+
return envelope.error?.code === ERROR_CODES.APPROVAL_UNKNOWN && approvalId2 ? { kind: "indeterminate", approvalId: approvalId2 } : { kind: "none" };
|
|
6250
|
+
}
|
|
6251
|
+
const report = envelope.result ?? {};
|
|
6252
|
+
const approvalId = readApprovalId(report.approvalId);
|
|
6253
|
+
if (!approvalId || report.status !== "resolved") {
|
|
6254
|
+
return { kind: "none" };
|
|
6255
|
+
}
|
|
6256
|
+
if (report.outcome !== "approved") {
|
|
6257
|
+
return { kind: "clear", approvalId };
|
|
6258
|
+
}
|
|
6259
|
+
const response = report.response;
|
|
6260
|
+
return isCommandResponseEnvelope(response, [MESSAGE_TYPES.COMMAND_RESPONSE]) ? { kind: "promote", approvalId, response } : { kind: "indeterminate", approvalId };
|
|
6261
|
+
}
|
|
6262
|
+
function settlementForCancel(envelope) {
|
|
6263
|
+
if (!envelope.ok) {
|
|
6264
|
+
return { kind: "none" };
|
|
6265
|
+
}
|
|
6266
|
+
const report = envelope.result ?? {};
|
|
6267
|
+
const approvalId = readApprovalId(report.approvalId);
|
|
6268
|
+
if (!approvalId) {
|
|
6269
|
+
return { kind: "none" };
|
|
6270
|
+
}
|
|
6271
|
+
if (report.status === "cancelled") {
|
|
6272
|
+
return { kind: "clear", approvalId };
|
|
6273
|
+
}
|
|
6274
|
+
return report.status === "resolved" ? { kind: "none" } : { kind: "indeterminate", approvalId };
|
|
6275
|
+
}
|
|
6276
|
+
function readApprovalSettlement(command, envelope) {
|
|
6277
|
+
if (command === APPROVAL_AWAIT_COMMAND) {
|
|
6278
|
+
return settlementForAwait(envelope);
|
|
6279
|
+
}
|
|
6280
|
+
if (command === APPROVAL_CANCEL_COMMAND) {
|
|
6281
|
+
return settlementForCancel(envelope);
|
|
6282
|
+
}
|
|
6283
|
+
return { kind: "none" };
|
|
6284
|
+
}
|
|
6285
|
+
var ApprovalIdempotencyLinks = class {
|
|
6286
|
+
maxEntries;
|
|
6287
|
+
retentionMs;
|
|
6288
|
+
now;
|
|
6289
|
+
links;
|
|
6290
|
+
keysByApprovalId;
|
|
6291
|
+
constructor({
|
|
6292
|
+
maxEntries = DEFAULT_MAX_ENTRIES,
|
|
6293
|
+
retentionMs = APPROVAL_RESULT_RETENTION_MS,
|
|
6294
|
+
now = () => Date.now()
|
|
6295
|
+
} = {}) {
|
|
6296
|
+
this.maxEntries = maxEntries;
|
|
6297
|
+
this.retentionMs = retentionMs;
|
|
6298
|
+
this.now = now;
|
|
6299
|
+
this.links = /* @__PURE__ */ new Map();
|
|
6300
|
+
this.keysByApprovalId = /* @__PURE__ */ new Map();
|
|
6301
|
+
}
|
|
6302
|
+
record({
|
|
6303
|
+
key,
|
|
6304
|
+
fingerprint,
|
|
6305
|
+
approvalId,
|
|
6306
|
+
expiresAt,
|
|
6307
|
+
pendingResponse
|
|
6308
|
+
}) {
|
|
6309
|
+
this.prune();
|
|
6310
|
+
this.forget(key);
|
|
6311
|
+
this.links.set(key, {
|
|
6312
|
+
key,
|
|
6313
|
+
kind: "link",
|
|
6314
|
+
fingerprint,
|
|
6315
|
+
approvalId,
|
|
6316
|
+
pendingResponse,
|
|
6317
|
+
expiresAt: expiresAt + this.retentionMs
|
|
6318
|
+
});
|
|
6319
|
+
this.keysByApprovalId.set(approvalId, key);
|
|
6320
|
+
}
|
|
6321
|
+
// The reservation is held for the pending request's whole lifetime, so a tombstone converts an
|
|
6322
|
+
// existing entry instead of competing for capacity with newer keys. recordLostInFlight stays
|
|
6323
|
+
// unconditional so that the boundary tick, where both expire at once, still retains the key.
|
|
6324
|
+
reserve({
|
|
6325
|
+
key,
|
|
6326
|
+
fingerprint,
|
|
6327
|
+
retainMs
|
|
6328
|
+
}) {
|
|
6329
|
+
this.prune();
|
|
6330
|
+
const existing = this.links.get(key);
|
|
6331
|
+
if (existing) {
|
|
6332
|
+
return existing.kind === "reserved" && existing.fingerprint === fingerprint ? { ok: true } : { ok: false, reason: "conflict" };
|
|
6333
|
+
}
|
|
6334
|
+
if (this.links.size >= this.maxEntries) {
|
|
6335
|
+
return { ok: false, reason: "full" };
|
|
6336
|
+
}
|
|
6337
|
+
this.links.set(key, {
|
|
6338
|
+
key,
|
|
6339
|
+
kind: "reserved",
|
|
6340
|
+
fingerprint,
|
|
6341
|
+
approvalId: null,
|
|
6342
|
+
pendingResponse: null,
|
|
6343
|
+
expiresAt: this.now() + retainMs
|
|
6344
|
+
});
|
|
6345
|
+
return { ok: true };
|
|
6346
|
+
}
|
|
6347
|
+
release(key) {
|
|
6348
|
+
if (this.links.get(key)?.kind === "reserved") {
|
|
6349
|
+
this.forget(key);
|
|
6350
|
+
}
|
|
6351
|
+
}
|
|
6352
|
+
recordLostInFlight({ key, fingerprint, retainMs }) {
|
|
6353
|
+
this.prune();
|
|
6354
|
+
const existing = this.links.get(key);
|
|
6355
|
+
if (existing && existing.kind !== "reserved") {
|
|
6356
|
+
return false;
|
|
6357
|
+
}
|
|
6358
|
+
this.links.set(key, {
|
|
6359
|
+
key,
|
|
6360
|
+
kind: "lost",
|
|
6361
|
+
fingerprint,
|
|
6362
|
+
approvalId: null,
|
|
6363
|
+
pendingResponse: null,
|
|
6364
|
+
expiresAt: this.now() + retainMs
|
|
6365
|
+
});
|
|
6366
|
+
return true;
|
|
6367
|
+
}
|
|
6368
|
+
lookup(key, fingerprint) {
|
|
6369
|
+
this.prune();
|
|
6370
|
+
const link = this.links.get(key);
|
|
6371
|
+
if (!link) {
|
|
6372
|
+
return { status: "miss" };
|
|
6373
|
+
}
|
|
6374
|
+
if (link.kind === "reserved") {
|
|
6375
|
+
return { status: "miss" };
|
|
6376
|
+
}
|
|
6377
|
+
if (link.fingerprint !== fingerprint) {
|
|
6378
|
+
return { status: "conflict", approvalId: link.approvalId };
|
|
6379
|
+
}
|
|
6380
|
+
if (link.approvalId === null) {
|
|
6381
|
+
return { status: "lost-in-flight" };
|
|
6382
|
+
}
|
|
6383
|
+
return link.pendingResponse ? { status: "pending", approvalId: link.approvalId, response: link.pendingResponse } : { status: "indeterminate", approvalId: link.approvalId };
|
|
6384
|
+
}
|
|
6385
|
+
settle(settlement) {
|
|
6386
|
+
if (settlement.kind === "none") {
|
|
6387
|
+
return null;
|
|
6388
|
+
}
|
|
6389
|
+
const key = this.keysByApprovalId.get(settlement.approvalId);
|
|
6390
|
+
const link = key === void 0 ? void 0 : this.links.get(key);
|
|
6391
|
+
if (!link) {
|
|
6392
|
+
return null;
|
|
6393
|
+
}
|
|
6394
|
+
if (settlement.kind === "indeterminate") {
|
|
6395
|
+
link.pendingResponse = null;
|
|
6396
|
+
return null;
|
|
6397
|
+
}
|
|
6398
|
+
const promoted = settlement.kind === "promote" ? { key: link.key, fingerprint: link.fingerprint } : null;
|
|
6399
|
+
this.forget(link.key);
|
|
6400
|
+
return promoted;
|
|
6401
|
+
}
|
|
6402
|
+
prune() {
|
|
6403
|
+
const now = this.now();
|
|
6404
|
+
for (const [key, link] of this.links) {
|
|
6405
|
+
if (link.expiresAt <= now) {
|
|
6406
|
+
this.forget(key);
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
get size() {
|
|
6411
|
+
return this.links.size;
|
|
6412
|
+
}
|
|
6413
|
+
clear() {
|
|
6414
|
+
this.links.clear();
|
|
6415
|
+
this.keysByApprovalId.clear();
|
|
6416
|
+
}
|
|
6417
|
+
forget(key) {
|
|
6418
|
+
const link = this.links.get(key);
|
|
6419
|
+
if (!link) {
|
|
6420
|
+
return;
|
|
6421
|
+
}
|
|
6422
|
+
this.links.delete(key);
|
|
6423
|
+
if (link.approvalId !== null && this.keysByApprovalId.get(link.approvalId) === key) {
|
|
6424
|
+
this.keysByApprovalId.delete(link.approvalId);
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
};
|
|
6428
|
+
|
|
5405
6429
|
// src/bridge/idempotency-cache.ts
|
|
5406
6430
|
import { createHash } from "node:crypto";
|
|
5407
6431
|
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
5408
|
-
var
|
|
6432
|
+
var DEFAULT_MAX_ENTRIES2 = 1e3;
|
|
5409
6433
|
function stableStringify(value) {
|
|
5410
6434
|
if (value === null || typeof value !== "object") {
|
|
5411
6435
|
return JSON.stringify(value) ?? "null";
|
|
@@ -5428,7 +6452,7 @@ var IdempotencyCache = class {
|
|
|
5428
6452
|
entries;
|
|
5429
6453
|
constructor({
|
|
5430
6454
|
ttlMs = DEFAULT_TTL_MS,
|
|
5431
|
-
maxEntries =
|
|
6455
|
+
maxEntries = DEFAULT_MAX_ENTRIES2,
|
|
5432
6456
|
now = () => Date.now()
|
|
5433
6457
|
} = {}) {
|
|
5434
6458
|
this.ttlMs = ttlMs;
|
|
@@ -5492,6 +6516,15 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 12e4;
|
|
|
5492
6516
|
function inFlightIndexKey(worldId, idempotencyKey) {
|
|
5493
6517
|
return JSON.stringify([worldId, idempotencyKey]);
|
|
5494
6518
|
}
|
|
6519
|
+
var UNDISCOVERABLE_COMMANDS = new Set(
|
|
6520
|
+
COMMAND_NAMES.filter((name) => !DISCOVERABLE_COMMAND_NAMES.includes(name))
|
|
6521
|
+
);
|
|
6522
|
+
function withoutUndiscoverableCommands(session) {
|
|
6523
|
+
return {
|
|
6524
|
+
...session,
|
|
6525
|
+
commands: session.commands.filter((name) => !UNDISCOVERABLE_COMMANDS.has(name))
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
5495
6528
|
var BridgeSessionStore = class {
|
|
5496
6529
|
activeBridgeSocket;
|
|
5497
6530
|
activeSession;
|
|
@@ -5505,20 +6538,18 @@ var BridgeSessionStore = class {
|
|
|
5505
6538
|
}
|
|
5506
6539
|
registerBridge(socket, session) {
|
|
5507
6540
|
const staleSocket = this.activeBridgeSocket && this.activeBridgeSocket !== socket ? this.activeBridgeSocket : null;
|
|
5508
|
-
|
|
5509
|
-
this.failPendingForBridge(staleSocket, "taken-over");
|
|
5510
|
-
}
|
|
6541
|
+
const lostKeyedRequests = staleSocket ? this.failPendingForBridge(staleSocket, "taken-over") : [];
|
|
5511
6542
|
this.activeBridgeSocket = socket;
|
|
5512
6543
|
this.activeSession = session;
|
|
5513
6544
|
if (staleSocket) {
|
|
5514
6545
|
staleSocket.close(BRIDGE_TAKEOVER_CLOSE_CODE, BRIDGE_TAKEOVER_CLOSE_REASON);
|
|
5515
6546
|
}
|
|
5516
|
-
return { ok: true };
|
|
6547
|
+
return { ok: true, lostKeyedRequests };
|
|
5517
6548
|
}
|
|
5518
6549
|
getBridgeStatus() {
|
|
5519
6550
|
return {
|
|
5520
6551
|
connected: Boolean(this.activeBridgeSocket && this.activeSession),
|
|
5521
|
-
session: this.activeSession
|
|
6552
|
+
session: this.activeSession ? withoutUndiscoverableCommands(this.activeSession) : null
|
|
5522
6553
|
};
|
|
5523
6554
|
}
|
|
5524
6555
|
hasActiveBridge() {
|
|
@@ -5597,7 +6628,7 @@ var BridgeSessionStore = class {
|
|
|
5597
6628
|
id: request.id,
|
|
5598
6629
|
error: createProtocolError({
|
|
5599
6630
|
code: ERROR_CODES.BRIDGE_TIMEOUT,
|
|
5600
|
-
message: `Timed out waiting for Foundry bridge response to ${request.command} after ${requestTimeoutMs}ms; the request was forwarded so it MAY have committed \u2014 verify world state before retrying (a same idempotencyKey retry can fetch the cached late success once it lands)`,
|
|
6631
|
+
message: `Timed out waiting for Foundry bridge response to ${request.command} after ${requestTimeoutMs}ms; the request was forwarded so it MAY have committed \u2014 verify world state before retrying (a same idempotencyKey retry can fetch the cached late success once it lands, unless the bridge session carrying it ends first, after which that key is refused)`,
|
|
5601
6632
|
details: {
|
|
5602
6633
|
reason: "timeout",
|
|
5603
6634
|
command: request.command,
|
|
@@ -5639,6 +6670,7 @@ var BridgeSessionStore = class {
|
|
|
5639
6670
|
}
|
|
5640
6671
|
}, requestTimeoutMs);
|
|
5641
6672
|
this.pendingRequests.set(request.id, {
|
|
6673
|
+
command: request.command,
|
|
5642
6674
|
waiters: [{ clientSocket, requestId: request.id }],
|
|
5643
6675
|
timeout,
|
|
5644
6676
|
bridgeSocket: this.activeBridgeSocket,
|
|
@@ -5654,7 +6686,7 @@ var BridgeSessionStore = class {
|
|
|
5654
6686
|
resolveResponse(response) {
|
|
5655
6687
|
const pendingRequest = this.pendingRequests.get(response.id);
|
|
5656
6688
|
if (!pendingRequest) {
|
|
5657
|
-
return { matched: false, idempotency: null, worldId: null };
|
|
6689
|
+
return { matched: false, command: null, idempotency: null, worldId: null };
|
|
5658
6690
|
}
|
|
5659
6691
|
clearTimeout(pendingRequest.timeout);
|
|
5660
6692
|
this.forgetPending(response.id);
|
|
@@ -5663,17 +6695,18 @@ var BridgeSessionStore = class {
|
|
|
5663
6695
|
}
|
|
5664
6696
|
return {
|
|
5665
6697
|
matched: true,
|
|
6698
|
+
command: pendingRequest.command,
|
|
5666
6699
|
idempotency: pendingRequest.idempotency ?? null,
|
|
5667
6700
|
worldId: pendingRequest.worldId ?? null
|
|
5668
6701
|
};
|
|
5669
6702
|
}
|
|
5670
6703
|
removeSocket(socket) {
|
|
5671
6704
|
if (this.activeBridgeSocket === socket) {
|
|
5672
|
-
this.failPendingForBridge(socket, "disconnected");
|
|
6705
|
+
const lostKeyedRequests = this.failPendingForBridge(socket, "disconnected");
|
|
5673
6706
|
this.activeBridgeSocket = null;
|
|
5674
6707
|
this.activeSession = null;
|
|
5675
6708
|
this.inFlightByKey.clear();
|
|
5676
|
-
return;
|
|
6709
|
+
return lostKeyedRequests;
|
|
5677
6710
|
}
|
|
5678
6711
|
for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
|
|
5679
6712
|
const remainingWaiters = pendingRequest.waiters.filter((waiter) => waiter.clientSocket !== socket);
|
|
@@ -5687,19 +6720,31 @@ var BridgeSessionStore = class {
|
|
|
5687
6720
|
clearTimeout(pendingRequest.timeout);
|
|
5688
6721
|
this.forgetPending(requestId);
|
|
5689
6722
|
}
|
|
6723
|
+
return [];
|
|
5690
6724
|
}
|
|
5691
6725
|
failPendingForBridge(socket, reason) {
|
|
6726
|
+
const lostKeyedRequests = [];
|
|
5692
6727
|
for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
|
|
5693
6728
|
if (pendingRequest.bridgeSocket !== socket) {
|
|
5694
6729
|
continue;
|
|
5695
6730
|
}
|
|
5696
6731
|
clearTimeout(pendingRequest.timeout);
|
|
5697
6732
|
this.forgetPending(requestId);
|
|
6733
|
+
const keyed = pendingRequest.idempotency !== void 0 && pendingRequest.worldId !== void 0;
|
|
6734
|
+
if (pendingRequest.idempotency && pendingRequest.worldId !== void 0) {
|
|
6735
|
+
lostKeyedRequests.push({
|
|
6736
|
+
worldId: pendingRequest.worldId,
|
|
6737
|
+
key: pendingRequest.idempotency.key,
|
|
6738
|
+
fingerprint: pendingRequest.idempotency.fingerprint
|
|
6739
|
+
});
|
|
6740
|
+
}
|
|
6741
|
+
const cause = reason === "taken-over" ? "Authenticated Foundry bridge session was replaced by a newer socket from the same pairing while the request was pending" : "Authenticated Foundry bridge session disconnected while the request was pending";
|
|
6742
|
+
const retryAdvice = keyed ? "verify world state before retrying, then send the command again under a FRESH idempotencyKey (reusing the same key is refused for the dedupe window on this daemon)" : "verify world state before retrying";
|
|
5698
6743
|
const disconnectError = createErrorResponse({
|
|
5699
6744
|
id: requestId,
|
|
5700
6745
|
error: createProtocolError({
|
|
5701
6746
|
code: ERROR_CODES.BRIDGE_DISCONNECTED,
|
|
5702
|
-
message:
|
|
6747
|
+
message: `${cause}; it was already forwarded so it MAY have committed \u2014 ${retryAdvice}`,
|
|
5703
6748
|
details: { reason }
|
|
5704
6749
|
})
|
|
5705
6750
|
});
|
|
@@ -5707,6 +6752,7 @@ var BridgeSessionStore = class {
|
|
|
5707
6752
|
sendJson(waiter.clientSocket, { ...disconnectError, id: waiter.requestId });
|
|
5708
6753
|
}
|
|
5709
6754
|
}
|
|
6755
|
+
return lostKeyedRequests;
|
|
5710
6756
|
}
|
|
5711
6757
|
clearAllPending() {
|
|
5712
6758
|
for (const pendingRequest of this.pendingRequests.values()) {
|
|
@@ -5721,6 +6767,22 @@ var BridgeSessionStore = class {
|
|
|
5721
6767
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
5722
6768
|
var DEFAULT_IDEMPOTENCY_TTL_MS = 10 * 60 * 1e3;
|
|
5723
6769
|
var ACCEPTED_BRIDGE_RESPONSE_TYPES = Object.freeze([MESSAGE_TYPES.COMMAND_RESPONSE]);
|
|
6770
|
+
var CLI_HELLO_ORIGIN = {
|
|
6771
|
+
peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
|
|
6772
|
+
handshake: PROTOCOL_HANDSHAKES.CLI_DAEMON
|
|
6773
|
+
};
|
|
6774
|
+
var MODULE_HELLO_ORIGIN = {
|
|
6775
|
+
peer: PROTOCOL_COMPONENTS.MODULE,
|
|
6776
|
+
handshake: PROTOCOL_HANDSHAKES.MODULE_DAEMON
|
|
6777
|
+
};
|
|
6778
|
+
var CLI_COMMAND_ORIGIN = {
|
|
6779
|
+
peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
|
|
6780
|
+
handshake: PROTOCOL_HANDSHAKES.COMMAND_REQUEST
|
|
6781
|
+
};
|
|
6782
|
+
var CLI_CONTROL_ORIGIN = {
|
|
6783
|
+
peer: PROTOCOL_COMPONENTS.CLI_DAEMON,
|
|
6784
|
+
handshake: PROTOCOL_HANDSHAKES.DAEMON_REQUEST
|
|
6785
|
+
};
|
|
5724
6786
|
function createBridgeHelloAck({
|
|
5725
6787
|
ok,
|
|
5726
6788
|
error,
|
|
@@ -5770,6 +6832,16 @@ function equalSecret(left, right) {
|
|
|
5770
6832
|
function daemonEnvelope(type, id, operation, ok, result, error) {
|
|
5771
6833
|
return { protocolVersion: PROTOCOL_VERSION, type, id, operation, ok, ...ok ? { result } : { error } };
|
|
5772
6834
|
}
|
|
6835
|
+
function idempotencyConflict(requestId, command, idempotencyKey, remedy, clause = "was already used for") {
|
|
6836
|
+
return createErrorResponse({
|
|
6837
|
+
id: requestId,
|
|
6838
|
+
error: createProtocolError({
|
|
6839
|
+
code: ERROR_CODES.IDEMPOTENCY_KEY_CONFLICT,
|
|
6840
|
+
message: `Idempotency key "${idempotencyKey}" ${clause} a different ${command} request (same key, different command or params); use a fresh idempotencyKey for this new request, or ${remedy}`,
|
|
6841
|
+
details: { command, idempotencyKey }
|
|
6842
|
+
})
|
|
6843
|
+
});
|
|
6844
|
+
}
|
|
5773
6845
|
function publicPendingPairing(entry) {
|
|
5774
6846
|
return {
|
|
5775
6847
|
code: entry.code,
|
|
@@ -5801,6 +6873,7 @@ var BridgeDaemon = class {
|
|
|
5801
6873
|
wsMaxPayloadBytes;
|
|
5802
6874
|
sessionStore;
|
|
5803
6875
|
idempotencyCache;
|
|
6876
|
+
approvalLinks;
|
|
5804
6877
|
cachedWorldId;
|
|
5805
6878
|
server;
|
|
5806
6879
|
connectionStates;
|
|
@@ -5814,6 +6887,7 @@ var BridgeDaemon = class {
|
|
|
5814
6887
|
heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
5815
6888
|
idempotencyTtlMs = DEFAULT_IDEMPOTENCY_TTL_MS,
|
|
5816
6889
|
idempotencyCacheOptions = {},
|
|
6890
|
+
approvalLinkOptions = {},
|
|
5817
6891
|
uploadLimitBytes = DEFAULT_UPLOAD_SIZE_LIMIT_BYTES,
|
|
5818
6892
|
logger = pino({ level: process.env.FVTT_WORLD_CLI_LOG_LEVEL ?? "warn" })
|
|
5819
6893
|
} = {}) {
|
|
@@ -5844,6 +6918,7 @@ var BridgeDaemon = class {
|
|
|
5844
6918
|
...idempotencyCacheOptions,
|
|
5845
6919
|
ttlMs: idempotencyCacheOptions.ttlMs ?? idempotencyTtlMs
|
|
5846
6920
|
});
|
|
6921
|
+
this.approvalLinks = new ApprovalIdempotencyLinks(approvalLinkOptions);
|
|
5847
6922
|
this.cachedWorldId = null;
|
|
5848
6923
|
this.server = null;
|
|
5849
6924
|
this.connectionStates = /* @__PURE__ */ new WeakMap();
|
|
@@ -5902,7 +6977,7 @@ var BridgeDaemon = class {
|
|
|
5902
6977
|
this.leaseExpiresAt = Date.now() + BRIDGE_LEASE_MS;
|
|
5903
6978
|
this.activePairingId = null;
|
|
5904
6979
|
}
|
|
5905
|
-
this.sessionStore.removeSocket(socket);
|
|
6980
|
+
this.tombstoneLostKeyedRequests(this.sessionStore.removeSocket(socket));
|
|
5906
6981
|
});
|
|
5907
6982
|
});
|
|
5908
6983
|
await new Promise((resolve7, reject) => {
|
|
@@ -5951,6 +7026,7 @@ var BridgeDaemon = class {
|
|
|
5951
7026
|
this.heartbeatTimer = null;
|
|
5952
7027
|
}
|
|
5953
7028
|
this.idempotencyCache.clear();
|
|
7029
|
+
this.approvalLinks.clear();
|
|
5954
7030
|
this.cachedWorldId = null;
|
|
5955
7031
|
this.dropAwaitWaiters(() => true);
|
|
5956
7032
|
this.sessionStore.clearAllPending();
|
|
@@ -5992,7 +7068,10 @@ var BridgeDaemon = class {
|
|
|
5992
7068
|
const message = parsed.value;
|
|
5993
7069
|
const validationResult = validateTransportMessage(message);
|
|
5994
7070
|
if (!validationResult.ok) {
|
|
5995
|
-
const
|
|
7071
|
+
const rejection = (origin) => message.protocolVersion !== void 0 && message.protocolVersion !== PROTOCOL_VERSION ? getProtocolVersionError(String(message.protocolVersion), {
|
|
7072
|
+
...origin,
|
|
7073
|
+
reporter: PROTOCOL_COMPONENTS.CLI_DAEMON
|
|
7074
|
+
}) : createProtocolError({
|
|
5996
7075
|
code: ERROR_CODES.INVALID_MESSAGE,
|
|
5997
7076
|
message: "Invalid transport message",
|
|
5998
7077
|
details: { errors: validationResult.errors }
|
|
@@ -6002,23 +7081,23 @@ var BridgeDaemon = class {
|
|
|
6002
7081
|
protocolVersion: PROTOCOL_VERSION,
|
|
6003
7082
|
type: MESSAGE_TYPES.CLIENT_HELLO_ACK,
|
|
6004
7083
|
ok: false,
|
|
6005
|
-
error:
|
|
7084
|
+
error: rejection(CLI_HELLO_ORIGIN)
|
|
6006
7085
|
});
|
|
6007
7086
|
} else if (state.role === "unknown" && message.type === MESSAGE_TYPES.PAIRING_REQUEST) {
|
|
6008
7087
|
sendJson(socket, {
|
|
6009
7088
|
protocolVersion: PROTOCOL_VERSION,
|
|
6010
7089
|
type: MESSAGE_TYPES.PAIRING_RESULT,
|
|
6011
7090
|
ok: false,
|
|
6012
|
-
error:
|
|
7091
|
+
error: rejection(MODULE_HELLO_ORIGIN)
|
|
6013
7092
|
});
|
|
6014
7093
|
} else if (state.role === "unknown" && message.type === MESSAGE_TYPES.BRIDGE_HELLO) {
|
|
6015
|
-
sendJson(socket, createBridgeHelloAck({ ok: false, error:
|
|
7094
|
+
sendJson(socket, createBridgeHelloAck({ ok: false, error: rejection(MODULE_HELLO_ORIGIN) }));
|
|
6016
7095
|
} else if (state.role === "cli" && message.type === MESSAGE_TYPES.COMMAND_REQUEST) {
|
|
6017
7096
|
sendJson(
|
|
6018
7097
|
socket,
|
|
6019
7098
|
createErrorResponse({
|
|
6020
7099
|
id: typeof message.id === "string" ? message.id : `invalid_${randomUUID2()}`,
|
|
6021
|
-
error:
|
|
7100
|
+
error: rejection(CLI_COMMAND_ORIGIN)
|
|
6022
7101
|
})
|
|
6023
7102
|
);
|
|
6024
7103
|
return;
|
|
@@ -6027,7 +7106,14 @@ var BridgeDaemon = class {
|
|
|
6027
7106
|
const operation = typeof message.operation === "string" && message.operation ? message.operation : "unknown";
|
|
6028
7107
|
sendJson(
|
|
6029
7108
|
socket,
|
|
6030
|
-
daemonEnvelope(
|
|
7109
|
+
daemonEnvelope(
|
|
7110
|
+
MESSAGE_TYPES.DAEMON_RESPONSE,
|
|
7111
|
+
id,
|
|
7112
|
+
operation,
|
|
7113
|
+
false,
|
|
7114
|
+
void 0,
|
|
7115
|
+
rejection(CLI_CONTROL_ORIGIN)
|
|
7116
|
+
)
|
|
6031
7117
|
);
|
|
6032
7118
|
return;
|
|
6033
7119
|
}
|
|
@@ -6143,7 +7229,7 @@ var BridgeDaemon = class {
|
|
|
6143
7229
|
state.role = "unknown";
|
|
6144
7230
|
state.pairingId = null;
|
|
6145
7231
|
}
|
|
6146
|
-
this.sessionStore.removeSocket(socket);
|
|
7232
|
+
this.tombstoneLostKeyedRequests(this.sessionStore.removeSocket(socket));
|
|
6147
7233
|
socket.close(closeCode, closeReason);
|
|
6148
7234
|
}
|
|
6149
7235
|
stampPairingLastSeen(pairingId) {
|
|
@@ -6407,9 +7493,11 @@ var BridgeDaemon = class {
|
|
|
6407
7493
|
const idempotencyScope = `${pairing.pairingId}:${pairing.origin}:${newSession.world.id}`;
|
|
6408
7494
|
if (this.cachedWorldId !== null && this.cachedWorldId !== idempotencyScope) {
|
|
6409
7495
|
this.idempotencyCache.clear();
|
|
7496
|
+
this.approvalLinks.clear();
|
|
6410
7497
|
}
|
|
6411
7498
|
this.cachedWorldId = idempotencyScope;
|
|
6412
|
-
this.sessionStore.registerBridge(socket, newSession);
|
|
7499
|
+
const { lostKeyedRequests } = this.sessionStore.registerBridge(socket, newSession);
|
|
7500
|
+
this.tombstoneLostKeyedRequests(lostKeyedRequests);
|
|
6413
7501
|
sendJson(
|
|
6414
7502
|
socket,
|
|
6415
7503
|
createBridgeHelloAck({
|
|
@@ -6423,9 +7511,51 @@ var BridgeDaemon = class {
|
|
|
6423
7511
|
this.logger.warn({ message }, "Ignoring non-response message from bridge socket");
|
|
6424
7512
|
return;
|
|
6425
7513
|
}
|
|
6426
|
-
const { idempotency, worldId: matchedWorldId } = this.sessionStore.resolveResponse(message);
|
|
6427
|
-
|
|
7514
|
+
const { command, idempotency, worldId: matchedWorldId } = this.sessionStore.resolveResponse(message);
|
|
7515
|
+
const inScope = socket === this.sessionStore.activeBridgeSocket || matchedWorldId !== null && matchedWorldId === this.cachedWorldId;
|
|
7516
|
+
if (!inScope) {
|
|
7517
|
+
return;
|
|
7518
|
+
}
|
|
7519
|
+
if (idempotency && message.ok === true) {
|
|
6428
7520
|
this.idempotencyCache.storeIfAbsent(idempotency.key, idempotency.fingerprint, message);
|
|
7521
|
+
this.approvalLinks.release(idempotency.key);
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7524
|
+
if (idempotency) {
|
|
7525
|
+
const pendingApproval = readPendingApprovalDetails(message);
|
|
7526
|
+
if (pendingApproval) {
|
|
7527
|
+
this.approvalLinks.record({
|
|
7528
|
+
key: idempotency.key,
|
|
7529
|
+
fingerprint: idempotency.fingerprint,
|
|
7530
|
+
pendingResponse: message,
|
|
7531
|
+
...pendingApproval
|
|
7532
|
+
});
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
this.approvalLinks.release(idempotency.key);
|
|
7536
|
+
return;
|
|
7537
|
+
}
|
|
7538
|
+
const settlement = readApprovalSettlement(command, message);
|
|
7539
|
+
const promoted = this.approvalLinks.settle(settlement);
|
|
7540
|
+
if (promoted && settlement.kind === "promote") {
|
|
7541
|
+
this.idempotencyCache.storeIfAbsent(promoted.key, promoted.fingerprint, settlement.response);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
tombstoneLostKeyedRequests(lostKeyedRequests) {
|
|
7545
|
+
for (const lost of lostKeyedRequests) {
|
|
7546
|
+
if (lost.worldId !== this.cachedWorldId) {
|
|
7547
|
+
this.approvalLinks.release(lost.key);
|
|
7548
|
+
continue;
|
|
7549
|
+
}
|
|
7550
|
+
if (this.idempotencyCache.lookup(lost.key, lost.fingerprint).status !== "miss") {
|
|
7551
|
+
this.approvalLinks.release(lost.key);
|
|
7552
|
+
continue;
|
|
7553
|
+
}
|
|
7554
|
+
this.approvalLinks.recordLostInFlight({
|
|
7555
|
+
key: lost.key,
|
|
7556
|
+
fingerprint: lost.fingerprint,
|
|
7557
|
+
retainMs: this.idempotencyCache.ttlMs
|
|
7558
|
+
});
|
|
6429
7559
|
}
|
|
6430
7560
|
}
|
|
6431
7561
|
handleCliMessage(socket, state, message) {
|
|
@@ -6459,14 +7589,40 @@ var BridgeDaemon = class {
|
|
|
6459
7589
|
return;
|
|
6460
7590
|
}
|
|
6461
7591
|
if (lookup.status === "conflict") {
|
|
7592
|
+
sendJson(
|
|
7593
|
+
socket,
|
|
7594
|
+
idempotencyConflict(
|
|
7595
|
+
requestId,
|
|
7596
|
+
String(message.command),
|
|
7597
|
+
idempotencyKey,
|
|
7598
|
+
"resend the byte-identical original request to fetch its cached result"
|
|
7599
|
+
)
|
|
7600
|
+
);
|
|
7601
|
+
return;
|
|
7602
|
+
}
|
|
7603
|
+
const link = this.approvalLinks.lookup(idempotencyKey, fingerprint);
|
|
7604
|
+
if (link.status === "conflict") {
|
|
7605
|
+
sendJson(
|
|
7606
|
+
socket,
|
|
7607
|
+
idempotencyConflict(
|
|
7608
|
+
requestId,
|
|
7609
|
+
String(message.command),
|
|
7610
|
+
idempotencyKey,
|
|
7611
|
+
link.approvalId === null ? "verify world state and use a fresh idempotencyKey" : "resend the byte-identical original request to keep waiting on the approval it already created"
|
|
7612
|
+
)
|
|
7613
|
+
);
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
if (link.status === "lost-in-flight") {
|
|
6462
7617
|
sendJson(
|
|
6463
7618
|
socket,
|
|
6464
7619
|
createErrorResponse({
|
|
6465
7620
|
id: requestId,
|
|
6466
7621
|
error: createProtocolError({
|
|
6467
|
-
code: ERROR_CODES.
|
|
6468
|
-
message: `Idempotency key "${idempotencyKey}"
|
|
7622
|
+
code: ERROR_CODES.BRIDGE_DISCONNECTED,
|
|
7623
|
+
message: `Idempotency key "${idempotencyKey}" belongs to a ${message.command} request that was already forwarded to Foundry when the bridge session ended, so this daemon never learned whether it ran: it may have changed the world, and it may already be waiting on a GM approval that is still actionable. Read the documents that request would have written before anything else, report what you found, and send the command again only under a fresh idempotencyKey.`,
|
|
6469
7624
|
details: {
|
|
7625
|
+
reason: "lost-in-flight",
|
|
6470
7626
|
command: message.command,
|
|
6471
7627
|
idempotencyKey
|
|
6472
7628
|
}
|
|
@@ -6475,6 +7631,28 @@ var BridgeDaemon = class {
|
|
|
6475
7631
|
);
|
|
6476
7632
|
return;
|
|
6477
7633
|
}
|
|
7634
|
+
if (link.status === "pending") {
|
|
7635
|
+
sendJson(socket, { ...link.response, id: requestId });
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7638
|
+
if (link.status === "indeterminate") {
|
|
7639
|
+
sendJson(
|
|
7640
|
+
socket,
|
|
7641
|
+
createErrorResponse({
|
|
7642
|
+
id: requestId,
|
|
7643
|
+
error: createProtocolError({
|
|
7644
|
+
code: ERROR_CODES.APPROVAL_UNKNOWN,
|
|
7645
|
+
message: `Idempotency key "${idempotencyKey}" belongs to a ${message.command} request whose GM approval ended without a readable outcome, so this daemon cannot say whether it ran: it may never have started, or it may have completed and changed the world. Read the documents that request would have written before anything else, report what you found, and send the command again only under a fresh idempotencyKey.`,
|
|
7646
|
+
details: {
|
|
7647
|
+
command: message.command,
|
|
7648
|
+
idempotencyKey,
|
|
7649
|
+
approvalId: link.approvalId
|
|
7650
|
+
}
|
|
7651
|
+
})
|
|
7652
|
+
})
|
|
7653
|
+
);
|
|
7654
|
+
return;
|
|
7655
|
+
}
|
|
6478
7656
|
if (this.cachedWorldId !== null) {
|
|
6479
7657
|
const inFlight = this.sessionStore.findInFlight(this.cachedWorldId, idempotencyKey);
|
|
6480
7658
|
if (inFlight) {
|
|
@@ -6489,22 +7667,52 @@ var BridgeDaemon = class {
|
|
|
6489
7667
|
} else {
|
|
6490
7668
|
sendJson(
|
|
6491
7669
|
socket,
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
idempotencyKey
|
|
6500
|
-
}
|
|
6501
|
-
})
|
|
6502
|
-
})
|
|
7670
|
+
idempotencyConflict(
|
|
7671
|
+
requestId,
|
|
7672
|
+
String(message.command),
|
|
7673
|
+
idempotencyKey,
|
|
7674
|
+
"resend the byte-identical original request to coalesce onto the in-flight one",
|
|
7675
|
+
"is already in flight for"
|
|
7676
|
+
)
|
|
6503
7677
|
);
|
|
6504
7678
|
return;
|
|
6505
7679
|
}
|
|
6506
7680
|
}
|
|
6507
7681
|
}
|
|
7682
|
+
if (this.cachedWorldId !== null) {
|
|
7683
|
+
const reserved = this.approvalLinks.reserve({
|
|
7684
|
+
key: idempotencyKey,
|
|
7685
|
+
fingerprint,
|
|
7686
|
+
retainMs: this.requestTimeoutMs + this.idempotencyCache.ttlMs
|
|
7687
|
+
});
|
|
7688
|
+
if (!reserved.ok && reserved.reason === "conflict") {
|
|
7689
|
+
sendJson(
|
|
7690
|
+
socket,
|
|
7691
|
+
idempotencyConflict(
|
|
7692
|
+
requestId,
|
|
7693
|
+
String(message.command),
|
|
7694
|
+
idempotencyKey,
|
|
7695
|
+
"resend the byte-identical original request to reuse the reservation it already holds",
|
|
7696
|
+
"is still reserved for"
|
|
7697
|
+
)
|
|
7698
|
+
);
|
|
7699
|
+
return;
|
|
7700
|
+
}
|
|
7701
|
+
if (!reserved.ok) {
|
|
7702
|
+
sendJson(
|
|
7703
|
+
socket,
|
|
7704
|
+
createErrorResponse({
|
|
7705
|
+
id: requestId,
|
|
7706
|
+
error: createProtocolError({
|
|
7707
|
+
code: ERROR_CODES.IDEMPOTENCY_STORE_FULL,
|
|
7708
|
+
message: `This daemon is holding its maximum of ${this.approvalLinks.maxEntries} indeterminate idempotency keys, so it cannot guarantee that a ${message.command} request forwarded under key "${idempotencyKey}" would stay deduplicated if the bridge session ended mid-flight; nothing was forwarded and the world is unchanged. Retry once earlier keys settle or their dedupe window expires.`,
|
|
7709
|
+
details: { command: message.command, idempotencyKey }
|
|
7710
|
+
})
|
|
7711
|
+
})
|
|
7712
|
+
);
|
|
7713
|
+
return;
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
6508
7716
|
idempotency = { key: idempotencyKey, fingerprint };
|
|
6509
7717
|
}
|
|
6510
7718
|
const result = this.sessionStore.forwardRequest({
|
|
@@ -6516,6 +7724,9 @@ var BridgeDaemon = class {
|
|
|
6516
7724
|
...idempotency && this.cachedWorldId !== null ? { worldId: this.cachedWorldId } : {}
|
|
6517
7725
|
});
|
|
6518
7726
|
if (!result.ok) {
|
|
7727
|
+
if (idempotency) {
|
|
7728
|
+
this.approvalLinks.release(idempotency.key);
|
|
7729
|
+
}
|
|
6519
7730
|
sendJson(
|
|
6520
7731
|
socket,
|
|
6521
7732
|
createErrorResponse({
|
|
@@ -6585,7 +7796,8 @@ async function sendCommand({
|
|
|
6585
7796
|
command,
|
|
6586
7797
|
params = {},
|
|
6587
7798
|
timeoutMs = DEFAULT_CLIENT_TIMEOUT_MS,
|
|
6588
|
-
maxPayloadBytes = DEFAULT_WS_MAX_PAYLOAD_BYTES
|
|
7799
|
+
maxPayloadBytes = DEFAULT_WS_MAX_PAYLOAD_BYTES,
|
|
7800
|
+
signal
|
|
6589
7801
|
}) {
|
|
6590
7802
|
warnIfClientTimeoutUnsafe(timeoutMs);
|
|
6591
7803
|
const built = buildValidatedRequest(command, params);
|
|
@@ -6613,11 +7825,32 @@ async function sendCommand({
|
|
|
6613
7825
|
}, timeoutMs);
|
|
6614
7826
|
function cleanup() {
|
|
6615
7827
|
clearTimeout(timeout);
|
|
7828
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6616
7829
|
socket.removeAllListeners();
|
|
6617
7830
|
if (socket.readyState === socket.OPEN || socket.readyState === socket.CONNECTING) {
|
|
6618
7831
|
socket.close();
|
|
6619
7832
|
}
|
|
6620
7833
|
}
|
|
7834
|
+
function onAbort() {
|
|
7835
|
+
if (settled) {
|
|
7836
|
+
return;
|
|
7837
|
+
}
|
|
7838
|
+
settled = true;
|
|
7839
|
+
cleanup();
|
|
7840
|
+
socket.terminate();
|
|
7841
|
+
reject(
|
|
7842
|
+
new DaemonTransportError(
|
|
7843
|
+
ERROR_CODES.DAEMON_UNAVAILABLE,
|
|
7844
|
+
`Stopped waiting for the daemon response to ${command}`,
|
|
7845
|
+
{ reason: "closed", command }
|
|
7846
|
+
)
|
|
7847
|
+
);
|
|
7848
|
+
}
|
|
7849
|
+
if (signal?.aborted) {
|
|
7850
|
+
onAbort();
|
|
7851
|
+
return;
|
|
7852
|
+
}
|
|
7853
|
+
signal?.addEventListener("abort", onAbort);
|
|
6621
7854
|
socket.once("open", () => {
|
|
6622
7855
|
opened = true;
|
|
6623
7856
|
socket.send(JSON.stringify(createClientHello({ credential: deviceCredential ?? "" })));
|
|
@@ -6739,6 +7972,13 @@ async function connectDaemonClient({
|
|
|
6739
7972
|
});
|
|
6740
7973
|
socket.once("message", (data) => {
|
|
6741
7974
|
const parsed = parseBridgeMessage(normalizeIncomingData(data));
|
|
7975
|
+
const skew = parsed.ok ? protocolVersionSkewError(parsed.value) : null;
|
|
7976
|
+
if (skew) {
|
|
7977
|
+
clearTimeout(timer);
|
|
7978
|
+
reject(skew);
|
|
7979
|
+
socket.close();
|
|
7980
|
+
return;
|
|
7981
|
+
}
|
|
6742
7982
|
const validation = parsed.ok ? validateTransportMessage(parsed.value) : { ok: false };
|
|
6743
7983
|
const message = parsed.ok && validation.ok ? parsed.value : null;
|
|
6744
7984
|
if (!message || message.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK || !message.ok) {
|
|
@@ -6820,7 +8060,7 @@ async function connectDaemonClient({
|
|
|
6820
8060
|
);
|
|
6821
8061
|
});
|
|
6822
8062
|
return {
|
|
6823
|
-
async send({ command, params = {}, timeoutMs: timeoutMs2 = DEFAULT_CLIENT_TIMEOUT_MS }) {
|
|
8063
|
+
async send({ command, params = {}, timeoutMs: timeoutMs2 = DEFAULT_CLIENT_TIMEOUT_MS, signal }) {
|
|
6824
8064
|
warnIfClientTimeoutUnsafe(timeoutMs2);
|
|
6825
8065
|
if (closedError) {
|
|
6826
8066
|
throw closedError;
|
|
@@ -6831,8 +8071,21 @@ async function connectDaemonClient({
|
|
|
6831
8071
|
}
|
|
6832
8072
|
const request = built.request;
|
|
6833
8073
|
return await new Promise((resolve7, reject) => {
|
|
8074
|
+
function onAbort() {
|
|
8075
|
+
clearTimeout(timer);
|
|
8076
|
+
pending.delete(request.id);
|
|
8077
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8078
|
+
reject(
|
|
8079
|
+
new DaemonTransportError(
|
|
8080
|
+
ERROR_CODES.DAEMON_UNAVAILABLE,
|
|
8081
|
+
`Stopped waiting for the daemon response to ${command}`,
|
|
8082
|
+
{ reason: "closed", command }
|
|
8083
|
+
)
|
|
8084
|
+
);
|
|
8085
|
+
}
|
|
6834
8086
|
const timer = setTimeout(() => {
|
|
6835
8087
|
pending.delete(request.id);
|
|
8088
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6836
8089
|
reject(
|
|
6837
8090
|
new DaemonTransportError(
|
|
6838
8091
|
ERROR_CODES.DAEMON_UNAVAILABLE,
|
|
@@ -6841,7 +8094,22 @@ async function connectDaemonClient({
|
|
|
6841
8094
|
)
|
|
6842
8095
|
);
|
|
6843
8096
|
}, timeoutMs2);
|
|
6844
|
-
|
|
8097
|
+
if (signal?.aborted) {
|
|
8098
|
+
onAbort();
|
|
8099
|
+
return;
|
|
8100
|
+
}
|
|
8101
|
+
signal?.addEventListener("abort", onAbort);
|
|
8102
|
+
pending.set(request.id, {
|
|
8103
|
+
resolve: (envelope) => {
|
|
8104
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8105
|
+
resolve7(envelope);
|
|
8106
|
+
},
|
|
8107
|
+
reject: (error) => {
|
|
8108
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8109
|
+
reject(error);
|
|
8110
|
+
},
|
|
8111
|
+
timer
|
|
8112
|
+
});
|
|
6845
8113
|
socket.send(JSON.stringify(request));
|
|
6846
8114
|
});
|
|
6847
8115
|
},
|
|
@@ -7041,6 +8309,38 @@ function toTransportErrorEnvelope(error) {
|
|
|
7041
8309
|
error instanceof Error ? error.message : String(error)
|
|
7042
8310
|
);
|
|
7043
8311
|
}
|
|
8312
|
+
var TRANSPORT_DETAIL_REASONS = /* @__PURE__ */ new Set([
|
|
8313
|
+
"timeout",
|
|
8314
|
+
"disconnected",
|
|
8315
|
+
"invalid_json",
|
|
8316
|
+
"unexpected_type",
|
|
8317
|
+
"closed",
|
|
8318
|
+
"connect_error"
|
|
8319
|
+
]);
|
|
8320
|
+
function normalizeTransportMessage(message) {
|
|
8321
|
+
if (/ECONNREFUSED/.test(message)) {
|
|
8322
|
+
return "Could not connect to the daemon (connection refused). Is `fvtt-world-cli bridge serve` running?";
|
|
8323
|
+
}
|
|
8324
|
+
if (/ETIMEDOUT/.test(message)) {
|
|
8325
|
+
return "Could not connect to the daemon (connection timed out).";
|
|
8326
|
+
}
|
|
8327
|
+
if (/ENOTFOUND|EAI_AGAIN/.test(message)) {
|
|
8328
|
+
return "Could not resolve the daemon host.";
|
|
8329
|
+
}
|
|
8330
|
+
if (/ECONNRESET/.test(message)) {
|
|
8331
|
+
return "The daemon connection was reset.";
|
|
8332
|
+
}
|
|
8333
|
+
return message;
|
|
8334
|
+
}
|
|
8335
|
+
function renderProtocolError(error) {
|
|
8336
|
+
const code = error?.code ?? "UNKNOWN_ERROR";
|
|
8337
|
+
const message = normalizeTransportMessage(String(error?.message ?? "Unknown error"));
|
|
8338
|
+
const reason = error?.details?.reason;
|
|
8339
|
+
const showDetails = Boolean(error?.details) && !(typeof reason === "string" && TRANSPORT_DETAIL_REASONS.has(reason));
|
|
8340
|
+
const details = showDetails ? `
|
|
8341
|
+
${JSON.stringify(error?.details, null, 2)}` : "";
|
|
8342
|
+
return `${code}: ${message}${details}`;
|
|
8343
|
+
}
|
|
7044
8344
|
function planLocalError(error) {
|
|
7045
8345
|
if (error instanceof ExecConnectError) {
|
|
7046
8346
|
return {
|
|
@@ -7125,7 +8425,7 @@ function planCliErrorOutput(error, jsonMode, options = {}) {
|
|
|
7125
8425
|
}
|
|
7126
8426
|
|
|
7127
8427
|
// src/program.ts
|
|
7128
|
-
import { Command as Command10, Option as
|
|
8428
|
+
import { Command as Command10, Option as Option16 } from "commander";
|
|
7129
8429
|
|
|
7130
8430
|
// src/commands/actor.ts
|
|
7131
8431
|
import { Option as Option3 } from "commander";
|
|
@@ -7267,6 +8567,49 @@ function parseOwnershipUsers(value) {
|
|
|
7267
8567
|
}
|
|
7268
8568
|
return object;
|
|
7269
8569
|
}
|
|
8570
|
+
function parseJsonValue(value, label) {
|
|
8571
|
+
try {
|
|
8572
|
+
return JSON.parse(value);
|
|
8573
|
+
} catch (error) {
|
|
8574
|
+
throw new InvalidArgumentError(
|
|
8575
|
+
error instanceof Error ? `${label} must be a JSON literal \u2014 true, 42, "text", [..] or {..}, so a quoted string stays distinct from a boolean or a number: ${error.message}` : `${label} must be a JSON literal \u2014 true, 42, "text", [..] or {..}`
|
|
8576
|
+
);
|
|
8577
|
+
}
|
|
8578
|
+
}
|
|
8579
|
+
var USER_ROLES = [1, 2, 3, 4];
|
|
8580
|
+
function parseUserRole(value) {
|
|
8581
|
+
const trimmed = value.trim();
|
|
8582
|
+
const parsed = INTEGER_PATTERN.test(trimmed) ? Number(trimmed) : NaN;
|
|
8583
|
+
if (!Number.isInteger(parsed) || !USER_ROLES.includes(parsed)) {
|
|
8584
|
+
throw new InvalidArgumentError(
|
|
8585
|
+
`Expected a user role (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster), received ${value}`
|
|
8586
|
+
);
|
|
8587
|
+
}
|
|
8588
|
+
return parsed;
|
|
8589
|
+
}
|
|
8590
|
+
function parseUserPermissions(value) {
|
|
8591
|
+
const object = parseJsonObject(value, "--permissions-json");
|
|
8592
|
+
const names = Object.keys(object);
|
|
8593
|
+
if (names.length === 0) {
|
|
8594
|
+
throw new InvalidArgumentError("--permissions-json must name at least one permission");
|
|
8595
|
+
}
|
|
8596
|
+
for (const name of names) {
|
|
8597
|
+
const entry = object[name];
|
|
8598
|
+
if (typeof entry !== "boolean" && entry !== null) {
|
|
8599
|
+
throw new InvalidArgumentError(
|
|
8600
|
+
`--permissions-json values must be true (grant), false (revoke) or null (drop the override so the role default applies); ${name} = ${JSON.stringify(entry)}`
|
|
8601
|
+
);
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
return object;
|
|
8605
|
+
}
|
|
8606
|
+
function parseSettingItems(value, label) {
|
|
8607
|
+
const items = parseJsonObjectArray(value, label);
|
|
8608
|
+
if (items.length === 0) {
|
|
8609
|
+
throw new InvalidArgumentError(`${label} must contain at least one setting item`);
|
|
8610
|
+
}
|
|
8611
|
+
return items;
|
|
8612
|
+
}
|
|
7270
8613
|
function parseIdList(value) {
|
|
7271
8614
|
const ids = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
7272
8615
|
if (ids.length === 0) {
|
|
@@ -8438,6 +9781,120 @@ function createSceneRegionBehaviorCloneParams(options) {
|
|
|
8438
9781
|
})
|
|
8439
9782
|
};
|
|
8440
9783
|
}
|
|
9784
|
+
var EXECUTABLE_BEHAVIOR_TYPE = "executeMacro";
|
|
9785
|
+
function nullableField(key, value, cleared) {
|
|
9786
|
+
return cleared ? { [key]: null } : value !== void 0 ? { [key]: value } : {};
|
|
9787
|
+
}
|
|
9788
|
+
function buildUserFields(options) {
|
|
9789
|
+
return {
|
|
9790
|
+
...stringField("color", options.color),
|
|
9791
|
+
...stringField("pronouns", options.pronouns),
|
|
9792
|
+
...nullableField("avatar", options.avatar, options.clearAvatar),
|
|
9793
|
+
...nullableField("character", options.character, options.clearCharacter),
|
|
9794
|
+
...optionalJsonObject(options.dataJson, "--data-json") ?? {},
|
|
9795
|
+
...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
|
|
9796
|
+
};
|
|
9797
|
+
}
|
|
9798
|
+
function createSettingSetParams(options) {
|
|
9799
|
+
return { namespace: options.namespace, key: options.key, value: options.valueJson };
|
|
9800
|
+
}
|
|
9801
|
+
async function resolveSettingItems(options, dependencies) {
|
|
9802
|
+
if (options.itemsStdin) {
|
|
9803
|
+
if (dependencies.stdin.isTTY) {
|
|
9804
|
+
throw new CommanderError2(
|
|
9805
|
+
1,
|
|
9806
|
+
"fvtt-world-cli.settingItemsNoStdin",
|
|
9807
|
+
"--items-stdin requires the setting items piped on stdin as a JSON array; refusing to read from an interactive terminal."
|
|
9808
|
+
);
|
|
9809
|
+
}
|
|
9810
|
+
return parseSettingItems(await text(dependencies.stdin), "--items-stdin");
|
|
9811
|
+
}
|
|
9812
|
+
if (options.itemsJson === void 0) {
|
|
9813
|
+
throw new CommanderError2(
|
|
9814
|
+
1,
|
|
9815
|
+
"fvtt-world-cli.settingItemsRequired",
|
|
9816
|
+
"setting set-many needs the items: pass --items-json with a JSON array of {namespace,key,value} objects, or --items-stdin to read that array from stdin."
|
|
9817
|
+
);
|
|
9818
|
+
}
|
|
9819
|
+
return options.itemsJson;
|
|
9820
|
+
}
|
|
9821
|
+
function buildExecutableBehaviorFields(options) {
|
|
9822
|
+
const system = {
|
|
9823
|
+
...stringField("uuid", options.macroUuid),
|
|
9824
|
+
...options.events ? { events: options.events } : {},
|
|
9825
|
+
...booleanField("everyone", options.everyone),
|
|
9826
|
+
...options.systemJson ? parseJsonObject(options.systemJson, "--system-json") : {}
|
|
9827
|
+
};
|
|
9828
|
+
return {
|
|
9829
|
+
...stringField("name", options.name),
|
|
9830
|
+
...booleanField("disabled", options.disabled),
|
|
9831
|
+
...Object.keys(system).length > 0 ? { system } : {}
|
|
9832
|
+
};
|
|
9833
|
+
}
|
|
9834
|
+
function createExecutableBehaviorCreateParams(options) {
|
|
9835
|
+
return {
|
|
9836
|
+
sceneId: options.sceneId,
|
|
9837
|
+
regionId: options.regionId,
|
|
9838
|
+
data: {
|
|
9839
|
+
type: EXECUTABLE_BEHAVIOR_TYPE,
|
|
9840
|
+
...buildExecutableBehaviorFields(options),
|
|
9841
|
+
...optionalJsonObject(options.dataJson, "--data-json") ?? {}
|
|
9842
|
+
}
|
|
9843
|
+
};
|
|
9844
|
+
}
|
|
9845
|
+
function createExecutableBehaviorUpdateParams(options) {
|
|
9846
|
+
return {
|
|
9847
|
+
sceneId: options.sceneId,
|
|
9848
|
+
regionId: options.regionId,
|
|
9849
|
+
behaviorId: options.behaviorId,
|
|
9850
|
+
patch: assertNonEmptyPatch({
|
|
9851
|
+
...buildExecutableBehaviorFields(options),
|
|
9852
|
+
...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
|
|
9853
|
+
})
|
|
9854
|
+
};
|
|
9855
|
+
}
|
|
9856
|
+
function createExecutableBehaviorCloneParams(options) {
|
|
9857
|
+
return {
|
|
9858
|
+
sceneId: options.sceneId,
|
|
9859
|
+
regionId: options.regionId,
|
|
9860
|
+
behaviorId: options.behaviorId,
|
|
9861
|
+
...optionalPatch({
|
|
9862
|
+
...buildExecutableBehaviorFields(options),
|
|
9863
|
+
...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
|
|
9864
|
+
})
|
|
9865
|
+
};
|
|
9866
|
+
}
|
|
9867
|
+
function createMacroExecuteParams(options) {
|
|
9868
|
+
const scope = {
|
|
9869
|
+
...stringField("actorId", options.actorId),
|
|
9870
|
+
...stringField("sceneId", options.sceneId),
|
|
9871
|
+
...stringField("tokenId", options.tokenId),
|
|
9872
|
+
...jsonObjectField("args", options.argsJson, "--args-json")
|
|
9873
|
+
};
|
|
9874
|
+
return {
|
|
9875
|
+
macroId: options.macroId,
|
|
9876
|
+
...Object.keys(scope).length > 0 ? { scope } : {},
|
|
9877
|
+
...numberField("timeoutMs", options.macroTimeoutMs)
|
|
9878
|
+
};
|
|
9879
|
+
}
|
|
9880
|
+
function createUserCreateParams(options) {
|
|
9881
|
+
return {
|
|
9882
|
+
data: {
|
|
9883
|
+
name: options.name,
|
|
9884
|
+
...numberField("role", options.role),
|
|
9885
|
+
...buildUserFields(options)
|
|
9886
|
+
}
|
|
9887
|
+
};
|
|
9888
|
+
}
|
|
9889
|
+
function createUserUpdateParams(options) {
|
|
9890
|
+
return {
|
|
9891
|
+
userId: options.userId,
|
|
9892
|
+
patch: assertNonEmptyPatch({
|
|
9893
|
+
...stringField("name", options.name),
|
|
9894
|
+
...buildUserFields(options)
|
|
9895
|
+
})
|
|
9896
|
+
};
|
|
9897
|
+
}
|
|
8441
9898
|
function createFileUploadParams(options, uploadLimitBytes) {
|
|
8442
9899
|
return {
|
|
8443
9900
|
path: options.path,
|
|
@@ -8450,6 +9907,248 @@ function createFileUploadParams(options, uploadLimitBytes) {
|
|
|
8450
9907
|
import { createInterface } from "node:readline";
|
|
8451
9908
|
import { CommanderError as CommanderError3 } from "commander";
|
|
8452
9909
|
|
|
9910
|
+
// src/park-polling.ts
|
|
9911
|
+
var AWAIT_FLOOR_MARGIN_MS = 5e3;
|
|
9912
|
+
var AWAIT_EMPTY_POLL_DELAY_MS = 250;
|
|
9913
|
+
|
|
9914
|
+
// src/approval-wait.ts
|
|
9915
|
+
var APPROVAL_AWAIT_CLIENT_TIMEOUT_FLOOR_MS = APPROVAL_AWAIT_PARK_CAP_MS + AWAIT_FLOOR_MARGIN_MS;
|
|
9916
|
+
var APPROVAL_EMPTY_POLL_DELAY_MS = AWAIT_EMPTY_POLL_DELAY_MS;
|
|
9917
|
+
var APPROVAL_RETRY_BASE_DELAY_MS = 500;
|
|
9918
|
+
var APPROVAL_RETRY_MAX_DELAY_MS = 5e3;
|
|
9919
|
+
var APPROVAL_RETRY_MARGIN_MS = 3e4;
|
|
9920
|
+
var TRANSIENT_TRANSPORT_CODES = /* @__PURE__ */ new Set([
|
|
9921
|
+
ERROR_CODES.DAEMON_UNAVAILABLE,
|
|
9922
|
+
ERROR_CODES.BRIDGE_NOT_READY,
|
|
9923
|
+
ERROR_CODES.BRIDGE_DISCONNECTED,
|
|
9924
|
+
ERROR_CODES.BRIDGE_TIMEOUT
|
|
9925
|
+
]);
|
|
9926
|
+
function readPendingApproval(response) {
|
|
9927
|
+
const details = readPendingApprovalDetails(response);
|
|
9928
|
+
if (!details) {
|
|
9929
|
+
return null;
|
|
9930
|
+
}
|
|
9931
|
+
const command = response.error?.details?.command;
|
|
9932
|
+
return { ...details, command: typeof command === "string" ? command : "unknown" };
|
|
9933
|
+
}
|
|
9934
|
+
function describeTransportFailure(envelope) {
|
|
9935
|
+
return `${envelope.error?.code ?? ERROR_CODES.INTERNAL_ERROR}: ${envelope.error?.message ?? "unknown error"}`;
|
|
9936
|
+
}
|
|
9937
|
+
async function awaitApprovalOutcome({
|
|
9938
|
+
pendingResponse,
|
|
9939
|
+
send,
|
|
9940
|
+
stderr,
|
|
9941
|
+
timeoutMs,
|
|
9942
|
+
reconnect,
|
|
9943
|
+
onCancelRequested,
|
|
9944
|
+
signalScope = process,
|
|
9945
|
+
now = () => Date.now(),
|
|
9946
|
+
sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
9947
|
+
}) {
|
|
9948
|
+
const pending = readPendingApproval(pendingResponse);
|
|
9949
|
+
if (!pending) {
|
|
9950
|
+
return pendingResponse;
|
|
9951
|
+
}
|
|
9952
|
+
const { approvalId, expiresAt, command } = pending;
|
|
9953
|
+
const responseId = pendingResponse.id;
|
|
9954
|
+
const pollTimeoutMs = Math.max(timeoutMs ?? 0, APPROVAL_AWAIT_CLIENT_TIMEOUT_FLOOR_MS);
|
|
9955
|
+
const cancelTimeoutMs = timeoutMs ?? DEFAULT_CLIENT_TIMEOUT_MS;
|
|
9956
|
+
function terminal(code, message, details) {
|
|
9957
|
+
return { ...localErrorEnvelope(code, message, details), id: responseId };
|
|
9958
|
+
}
|
|
9959
|
+
function indeterminate(message, details) {
|
|
9960
|
+
return terminal(
|
|
9961
|
+
ERROR_CODES.APPROVAL_UNKNOWN,
|
|
9962
|
+
`${message} The outcome is indeterminate: command ${command} may never have started, may be running now, or may have completed and changed the world. Read the documents it would have written before anything else, report what you found, and if you decide to send it again, send it under a fresh idempotency key.`,
|
|
9963
|
+
{ approvalId, command, ...details }
|
|
9964
|
+
);
|
|
9965
|
+
}
|
|
9966
|
+
function outcomeEnvelope(outcome) {
|
|
9967
|
+
if (outcome === "denied") {
|
|
9968
|
+
return terminal(
|
|
9969
|
+
ERROR_CODES.APPROVAL_DENIED,
|
|
9970
|
+
`The GM of this bridge denied command ${command}. It never ran and no world state changed: a denial is decided before execution starts, so nothing was partially applied. The verdict is terminal for this invocation and is not a transient failure to retry in a loop. Report the denial to the user; sending the same command again only makes sense once that human decision changes.`,
|
|
9971
|
+
{ approvalId, command }
|
|
9972
|
+
);
|
|
9973
|
+
}
|
|
9974
|
+
if (outcome === "timeout") {
|
|
9975
|
+
return terminal(
|
|
9976
|
+
ERROR_CODES.APPROVAL_TIMEOUT,
|
|
9977
|
+
`The approval for command ${command} expired before a GM decided it. It never ran and no world state changed: the expiry is decided before execution starts. The verdict is terminal for this invocation. Before sending the command again, check that a GM is present at the Foundry client holding this bridge and able to answer its approval window, and tell the user the decision is waiting on them.`,
|
|
9978
|
+
{ approvalId, command }
|
|
9979
|
+
);
|
|
9980
|
+
}
|
|
9981
|
+
if (outcome === "cancelled") {
|
|
9982
|
+
return terminal(
|
|
9983
|
+
ERROR_CODES.APPROVAL_CANCELLED,
|
|
9984
|
+
`The approval for command ${command} was cancelled before the GM decided it. The cancellation won the decision, so the command never ran and no world state changed. The verdict is terminal for this invocation, and sending the command again is safe: it asks the GM for a new approval.`,
|
|
9985
|
+
{ approvalId, command }
|
|
9986
|
+
);
|
|
9987
|
+
}
|
|
9988
|
+
return indeterminate(
|
|
9989
|
+
`The GM client reported an approval outcome this CLI does not understand (${outcome}).`,
|
|
9990
|
+
{ outcome }
|
|
9991
|
+
);
|
|
9992
|
+
}
|
|
9993
|
+
function readAwaitResult(result) {
|
|
9994
|
+
const report = result ?? {};
|
|
9995
|
+
if (report.status === "pending") {
|
|
9996
|
+
return { kind: "pending" };
|
|
9997
|
+
}
|
|
9998
|
+
if (report.status !== "resolved") {
|
|
9999
|
+
return {
|
|
10000
|
+
kind: "settled",
|
|
10001
|
+
envelope: indeterminate("The GM client reported an approval status this CLI does not understand.", {
|
|
10002
|
+
status: typeof report.status === "string" ? report.status : null
|
|
10003
|
+
})
|
|
10004
|
+
};
|
|
10005
|
+
}
|
|
10006
|
+
if (report.outcome !== "approved") {
|
|
10007
|
+
return { kind: "settled", envelope: outcomeEnvelope(String(report.outcome)) };
|
|
10008
|
+
}
|
|
10009
|
+
if (!isCommandResponseEnvelope(report.response, [MESSAGE_TYPES.COMMAND_RESPONSE])) {
|
|
10010
|
+
return {
|
|
10011
|
+
kind: "settled",
|
|
10012
|
+
envelope: indeterminate("The GM allowed the command, but its response was not delivered.", {
|
|
10013
|
+
outcome: "approved"
|
|
10014
|
+
})
|
|
10015
|
+
};
|
|
10016
|
+
}
|
|
10017
|
+
return { kind: "settled", envelope: report.response };
|
|
10018
|
+
}
|
|
10019
|
+
async function pollOnce(signal, waitMs = APPROVAL_AWAIT_PARK_CAP_MS) {
|
|
10020
|
+
let response;
|
|
10021
|
+
try {
|
|
10022
|
+
response = await send({
|
|
10023
|
+
command: APPROVAL_AWAIT_COMMAND,
|
|
10024
|
+
params: { approvalId, waitMs },
|
|
10025
|
+
timeoutMs: pollTimeoutMs,
|
|
10026
|
+
...signal ? { signal } : {}
|
|
10027
|
+
});
|
|
10028
|
+
} catch (error) {
|
|
10029
|
+
return { kind: "transport", envelope: toTransportErrorEnvelope(error), connectionLost: true };
|
|
10030
|
+
}
|
|
10031
|
+
if (response.ok) {
|
|
10032
|
+
return readAwaitResult(response.result);
|
|
10033
|
+
}
|
|
10034
|
+
if (TRANSIENT_TRANSPORT_CODES.has(response.error?.code ?? "")) {
|
|
10035
|
+
return { kind: "transport", envelope: response, connectionLost: false };
|
|
10036
|
+
}
|
|
10037
|
+
return { kind: "settled", envelope: response };
|
|
10038
|
+
}
|
|
10039
|
+
async function requestCancellation() {
|
|
10040
|
+
let response;
|
|
10041
|
+
try {
|
|
10042
|
+
response = await send({
|
|
10043
|
+
command: APPROVAL_CANCEL_COMMAND,
|
|
10044
|
+
params: { approvalId },
|
|
10045
|
+
timeoutMs: cancelTimeoutMs
|
|
10046
|
+
});
|
|
10047
|
+
} catch (error) {
|
|
10048
|
+
return {
|
|
10049
|
+
kind: "settled",
|
|
10050
|
+
envelope: indeterminate(
|
|
10051
|
+
`Cancellation of command ${command} could not be confirmed: the GM client could not be reached (${describeTransportFailure(toTransportErrorEnvelope(error))}).`,
|
|
10052
|
+
{ cancellation: "unconfirmed" }
|
|
10053
|
+
)
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
if (!response.ok) {
|
|
10057
|
+
return {
|
|
10058
|
+
kind: "settled",
|
|
10059
|
+
envelope: indeterminate(
|
|
10060
|
+
`Cancellation of command ${command} could not be confirmed: the cancellation request itself failed (${describeTransportFailure(response)}).`,
|
|
10061
|
+
{ cancellation: "unconfirmed" }
|
|
10062
|
+
)
|
|
10063
|
+
};
|
|
10064
|
+
}
|
|
10065
|
+
const status = response.result?.status;
|
|
10066
|
+
if (status === "cancelled") {
|
|
10067
|
+
return { kind: "settled", envelope: outcomeEnvelope("cancelled") };
|
|
10068
|
+
}
|
|
10069
|
+
if (status === "resolved") {
|
|
10070
|
+
const settled = await pollOnce(void 0, 0);
|
|
10071
|
+
if (settled.kind === "settled") {
|
|
10072
|
+
return settled;
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
10075
|
+
return {
|
|
10076
|
+
kind: "settled",
|
|
10077
|
+
envelope: indeterminate(
|
|
10078
|
+
`Cancellation of command ${command} could not be confirmed: the GM client answered "${String(status)}", so the decision had already left the cancellable state.`,
|
|
10079
|
+
{ cancellation: "unconfirmed", status: typeof status === "string" ? status : null }
|
|
10080
|
+
)
|
|
10081
|
+
};
|
|
10082
|
+
}
|
|
10083
|
+
stderr.write(
|
|
10084
|
+
`Waiting for GM approval in Foundry (command ${command}, expires ${new Date(expiresAt).toISOString()}). Press Ctrl+C to request cancellation.
|
|
10085
|
+
`
|
|
10086
|
+
);
|
|
10087
|
+
let settleCancellation = () => {
|
|
10088
|
+
};
|
|
10089
|
+
const cancellation = new Promise((resolve7) => {
|
|
10090
|
+
settleCancellation = resolve7;
|
|
10091
|
+
});
|
|
10092
|
+
let cancelling = false;
|
|
10093
|
+
function onSignal() {
|
|
10094
|
+
signalScope.off("SIGINT", onSignal);
|
|
10095
|
+
if (cancelling) {
|
|
10096
|
+
return;
|
|
10097
|
+
}
|
|
10098
|
+
cancelling = true;
|
|
10099
|
+
onCancelRequested?.();
|
|
10100
|
+
void requestCancellation().then(settleCancellation);
|
|
10101
|
+
}
|
|
10102
|
+
async function raceCancellation(work) {
|
|
10103
|
+
return await Promise.race([work.then(() => null), cancellation]);
|
|
10104
|
+
}
|
|
10105
|
+
signalScope.on("SIGINT", onSignal);
|
|
10106
|
+
let pollAbort = null;
|
|
10107
|
+
try {
|
|
10108
|
+
let retryDelayMs = APPROVAL_RETRY_BASE_DELAY_MS;
|
|
10109
|
+
for (; ; ) {
|
|
10110
|
+
pollAbort = new AbortController();
|
|
10111
|
+
const step = await Promise.race([pollOnce(pollAbort.signal), cancellation]);
|
|
10112
|
+
if (step.kind === "settled") {
|
|
10113
|
+
return step.envelope;
|
|
10114
|
+
}
|
|
10115
|
+
if (step.kind === "pending") {
|
|
10116
|
+
retryDelayMs = APPROVAL_RETRY_BASE_DELAY_MS;
|
|
10117
|
+
const cancelled = await raceCancellation(sleep(APPROVAL_EMPTY_POLL_DELAY_MS));
|
|
10118
|
+
if (cancelled) {
|
|
10119
|
+
return cancelled.envelope;
|
|
10120
|
+
}
|
|
10121
|
+
continue;
|
|
10122
|
+
}
|
|
10123
|
+
if (now() > expiresAt + APPROVAL_RETRY_MARGIN_MS) {
|
|
10124
|
+
return step.envelope;
|
|
10125
|
+
}
|
|
10126
|
+
const cancelledWhileWaiting = await raceCancellation(sleep(retryDelayMs));
|
|
10127
|
+
if (cancelledWhileWaiting) {
|
|
10128
|
+
return cancelledWhileWaiting.envelope;
|
|
10129
|
+
}
|
|
10130
|
+
retryDelayMs = Math.min(retryDelayMs * 2, APPROVAL_RETRY_MAX_DELAY_MS);
|
|
10131
|
+
if (reconnect && step.connectionLost) {
|
|
10132
|
+
let failure = null;
|
|
10133
|
+
const cancelledWhileReconnecting = await raceCancellation(
|
|
10134
|
+
reconnect().catch((error) => {
|
|
10135
|
+
failure = error;
|
|
10136
|
+
})
|
|
10137
|
+
);
|
|
10138
|
+
if (cancelledWhileReconnecting) {
|
|
10139
|
+
return cancelledWhileReconnecting.envelope;
|
|
10140
|
+
}
|
|
10141
|
+
if (failure !== null && now() > expiresAt + APPROVAL_RETRY_MARGIN_MS) {
|
|
10142
|
+
return toTransportErrorEnvelope(failure);
|
|
10143
|
+
}
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
10146
|
+
} finally {
|
|
10147
|
+
pollAbort?.abort();
|
|
10148
|
+
signalScope.off("SIGINT", onSignal);
|
|
10149
|
+
}
|
|
10150
|
+
}
|
|
10151
|
+
|
|
8453
10152
|
// src/config-io.ts
|
|
8454
10153
|
function getStoredConfig(dependencies) {
|
|
8455
10154
|
const existing = dependencies.configStore.readConfig();
|
|
@@ -8514,6 +10213,15 @@ var OWNERSHIP_LEVEL_LABELS = {
|
|
|
8514
10213
|
"2": "observer",
|
|
8515
10214
|
"3": "owner"
|
|
8516
10215
|
};
|
|
10216
|
+
function renderBroadcastLines(result) {
|
|
10217
|
+
const requested = result?.userIds;
|
|
10218
|
+
return [
|
|
10219
|
+
`requested: ${Array.isArray(requested) ? requested.join(", ") || "(none)" : "every user"}`,
|
|
10220
|
+
`reached: ${(result?.activeUserIds ?? []).join(", ") || "(nobody online)"}`,
|
|
10221
|
+
`skipped (offline): ${(result?.inactiveUserIds ?? result?.skippedUserIds ?? []).join(", ") || "(none)"}`,
|
|
10222
|
+
`dispatched: ${Boolean(result?.dispatched)} \u2014 a broadcast writes nothing, so there is no state to confirm afterwards`
|
|
10223
|
+
];
|
|
10224
|
+
}
|
|
8517
10225
|
function renderCompendiumSourceLines(document) {
|
|
8518
10226
|
return document?.compendiumSource ? [`compendiumSource: ${document.compendiumSource}`] : [];
|
|
8519
10227
|
}
|
|
@@ -8955,6 +10663,15 @@ function renderCardsRecallLines(recall, { deleteScoped = true } = {}) {
|
|
|
8955
10663
|
}
|
|
8956
10664
|
|
|
8957
10665
|
// src/render/chat.ts
|
|
10666
|
+
function renderChatFlushResult(result) {
|
|
10667
|
+
return result?.dryRun ? [
|
|
10668
|
+
`[dry-run] would delete the entire chat log: ${result?.count ?? 0} message(s)`,
|
|
10669
|
+
`remaining after the flush: 0`
|
|
10670
|
+
].join("\n") : [
|
|
10671
|
+
`Flushed the chat log: deleted ${result?.deleted ?? 0} of ${result?.count ?? 0} message(s)`,
|
|
10672
|
+
`remaining: ${result?.remaining ?? 0}`
|
|
10673
|
+
].join("\n");
|
|
10674
|
+
}
|
|
8958
10675
|
function renderChatDetails(message) {
|
|
8959
10676
|
const lines = [
|
|
8960
10677
|
`id: ${message?.id}`,
|
|
@@ -9212,6 +10929,13 @@ function renderFolderDetails(folder) {
|
|
|
9212
10929
|
}
|
|
9213
10930
|
|
|
9214
10931
|
// src/render/journals.ts
|
|
10932
|
+
function renderJournalShowResult(result) {
|
|
10933
|
+
return [
|
|
10934
|
+
`Showed journal entry ${result?.journalId} to other users`,
|
|
10935
|
+
`force: ${Boolean(result?.force)}`,
|
|
10936
|
+
...renderBroadcastLines(result)
|
|
10937
|
+
].join("\n");
|
|
10938
|
+
}
|
|
9215
10939
|
function renderJournalDetails(journal) {
|
|
9216
10940
|
const pages = Array.isArray(journal?.pages) ? journal.pages : [];
|
|
9217
10941
|
const categories = Array.isArray(journal?.categories) ? journal.categories : [];
|
|
@@ -9297,6 +11021,36 @@ function renderJournalCategoryDetails(journalId, category) {
|
|
|
9297
11021
|
}
|
|
9298
11022
|
|
|
9299
11023
|
// src/render/macros.ts
|
|
11024
|
+
var CHAT_CAPTURE_NOTES = {
|
|
11025
|
+
captured: "captured (every message the macro created while the bridge waited)",
|
|
11026
|
+
partial: "partial (fewer messages than this macro was expected to create)",
|
|
11027
|
+
"not-created": "not-created (the macro created no chat message while the bridge waited)",
|
|
11028
|
+
unknown: "unknown (the macro may have created messages this call could not observe)"
|
|
11029
|
+
};
|
|
11030
|
+
function renderMacroExecuteResult(result) {
|
|
11031
|
+
if (result?.dryRun) {
|
|
11032
|
+
return [
|
|
11033
|
+
`[dry-run] would execute macro ${result?.macroId}`,
|
|
11034
|
+
`type: ${result?.type ?? "(unknown)"}`,
|
|
11035
|
+
`canExecute: ${Boolean(result?.canExecute)}`,
|
|
11036
|
+
`command length: ${result?.commandLength ?? 0} characters`
|
|
11037
|
+
].join("\n");
|
|
11038
|
+
}
|
|
11039
|
+
const lines = [
|
|
11040
|
+
`Executed macro ${result?.macroId}`,
|
|
11041
|
+
`type: ${result?.type ?? "(unknown)"}`,
|
|
11042
|
+
`returned: ${JSON.stringify(result?.returned ?? null, null, 2)}`
|
|
11043
|
+
];
|
|
11044
|
+
if (result?.returnedOmitted) {
|
|
11045
|
+
lines.push(`returned omitted: ${result.returnedOmitted.code} \u2014 ${result.returnedOmitted.message}`);
|
|
11046
|
+
}
|
|
11047
|
+
lines.push(
|
|
11048
|
+
`chat messages: ${(result?.chatMessageIds ?? []).join(", ") || "(none)"}`,
|
|
11049
|
+
`chat capture: ${CHAT_CAPTURE_NOTES[result?.chatCapture] ?? CHAT_CAPTURE_NOTES.unknown}`,
|
|
11050
|
+
"note: a macro that catches its own errors still returns null, so verify the effect with reads"
|
|
11051
|
+
);
|
|
11052
|
+
return lines.join("\n");
|
|
11053
|
+
}
|
|
9300
11054
|
function renderMacroDetails(macro) {
|
|
9301
11055
|
const lines = [
|
|
9302
11056
|
`id: ${macro?.id}`,
|
|
@@ -9354,6 +11108,21 @@ function renderPlaylistSoundDetails(sound) {
|
|
|
9354
11108
|
}
|
|
9355
11109
|
|
|
9356
11110
|
// src/render/scenes.ts
|
|
11111
|
+
function renderSceneActivateResult(result) {
|
|
11112
|
+
return [
|
|
11113
|
+
`${result?.dryRun ? "[dry-run] would activate scene" : "Activated scene"} ${result?.sceneId}`,
|
|
11114
|
+
`was active: ${Boolean(result?.wasActive)}`,
|
|
11115
|
+
`changed: ${Boolean(result?.changed)}`
|
|
11116
|
+
].join("\n");
|
|
11117
|
+
}
|
|
11118
|
+
function renderScenePullUsersResult(result) {
|
|
11119
|
+
return [
|
|
11120
|
+
`Pulled users to scene ${result?.sceneId}`,
|
|
11121
|
+
`pulled: ${(result?.userIds ?? []).join(", ") || "(nobody online)"}`,
|
|
11122
|
+
`skipped (offline): ${(result?.skippedUserIds ?? []).join(", ") || "(none)"}`,
|
|
11123
|
+
`dispatched: ${Boolean(result?.dispatched)} \u2014 a pull writes nothing, so there is no state to confirm afterwards`
|
|
11124
|
+
].join("\n");
|
|
11125
|
+
}
|
|
9357
11126
|
function renderSceneDetails(scene) {
|
|
9358
11127
|
const lines = [
|
|
9359
11128
|
`id: ${scene?.id}`,
|
|
@@ -9575,6 +11344,45 @@ function renderSettingSummaryLine(setting) {
|
|
|
9575
11344
|
...setting?.valueRedacted ? ["value=REDACTED"] : []
|
|
9576
11345
|
].join(" ");
|
|
9577
11346
|
}
|
|
11347
|
+
function renderSettingValue(value) {
|
|
11348
|
+
return JSON.stringify(value ?? null, null, 2);
|
|
11349
|
+
}
|
|
11350
|
+
function renderSettingWriteLines(row, dryRun) {
|
|
11351
|
+
const verb = dryRun ? "would write" : row?.changed ? "wrote" : "left unchanged";
|
|
11352
|
+
return [
|
|
11353
|
+
`${verb}: ${row?.id ?? `${row?.namespace}.${row?.key}`} (scope: ${row?.scope ?? "?"})`,
|
|
11354
|
+
`previous: ${renderSettingValue(row?.previous)}`,
|
|
11355
|
+
`${dryRun ? "requested" : "value"}: ${renderSettingValue(row?.value)}`,
|
|
11356
|
+
`changed: ${Boolean(row?.changed)} / validated: ${Boolean(row?.validated)}`,
|
|
11357
|
+
...row?.requiresReload ? ["requiresReload: true \u2014 Foundry acts on this only after `system reload` or a manual browser reload"] : ["requiresReload: false"]
|
|
11358
|
+
];
|
|
11359
|
+
}
|
|
11360
|
+
function renderSettingWriteResult(result) {
|
|
11361
|
+
return renderSettingWriteLines(result, Boolean(result?.dryRun)).join("\n");
|
|
11362
|
+
}
|
|
11363
|
+
function renderSettingWriteOutcomes(result) {
|
|
11364
|
+
const outcomes = result?.outcomes ?? [];
|
|
11365
|
+
const dryRun = Boolean(result?.dryRun);
|
|
11366
|
+
const lines = [
|
|
11367
|
+
`${dryRun ? "Would write" : "Wrote"} settings (${outcomes.length}) \u2014 complete: ${Boolean(result?.complete)}`,
|
|
11368
|
+
...outcomes.flatMap(
|
|
11369
|
+
(outcome) => outcome?.status === "updated" || outcome?.status === "unchanged" ? [
|
|
11370
|
+
`[${outcome.index}] ${outcome.status}`,
|
|
11371
|
+
...renderSettingWriteLines(outcome, dryRun).map((line) => ` ${line}`)
|
|
11372
|
+
] : [`[${outcome?.index}] ${outcome?.status} ${outcome?.id ?? ""}`]
|
|
11373
|
+
)
|
|
11374
|
+
];
|
|
11375
|
+
if (result?.failure) {
|
|
11376
|
+
lines.push(`failure: ${result.failure.code} \u2014 ${result.failure.message}`);
|
|
11377
|
+
}
|
|
11378
|
+
return lines.join("\n");
|
|
11379
|
+
}
|
|
11380
|
+
function renderSettingBatchRows(setting) {
|
|
11381
|
+
return [
|
|
11382
|
+
renderSettingSummaryLine(setting),
|
|
11383
|
+
...setting?.error ? [` error: ${setting.error.code} \u2014 ${setting.error.message}`] : [` value: ${renderSettingValue(setting?.value)}`]
|
|
11384
|
+
];
|
|
11385
|
+
}
|
|
9578
11386
|
function renderSettingDetails(setting) {
|
|
9579
11387
|
return [
|
|
9580
11388
|
`setting: ${setting?.id ?? `${setting?.namespace}.${setting?.key}`}`,
|
|
@@ -9684,6 +11492,38 @@ function renderUserSummaryLine(user) {
|
|
|
9684
11492
|
`char=${user?.character ?? "-"}`
|
|
9685
11493
|
].join(" ");
|
|
9686
11494
|
}
|
|
11495
|
+
function renderUserRoleSetResult(result) {
|
|
11496
|
+
const dryRun = Boolean(result?.dryRun);
|
|
11497
|
+
return [
|
|
11498
|
+
`${dryRun ? "Would set" : "Set"} role for user ${result?.userId}`,
|
|
11499
|
+
`role: ${result?.previousRole ?? "?"} \u2192 ${result?.role} (${result?.roleName ?? "?"})`,
|
|
11500
|
+
`changed: ${Boolean(result?.changed)}`
|
|
11501
|
+
].join("\n");
|
|
11502
|
+
}
|
|
11503
|
+
function renderUserPermissionsSetResult(result) {
|
|
11504
|
+
const dryRun = Boolean(result?.dryRun);
|
|
11505
|
+
const overrides = Object.entries(result?.overrides ?? {});
|
|
11506
|
+
const lines = [
|
|
11507
|
+
`${dryRun ? "Would set" : "Set"} permission overrides for user ${result?.userId}`,
|
|
11508
|
+
`role: ${result?.role ?? "?"}`
|
|
11509
|
+
];
|
|
11510
|
+
if (dryRun) {
|
|
11511
|
+
lines.push(
|
|
11512
|
+
"requested:",
|
|
11513
|
+
...Object.entries(result?.requested ?? {}).map(
|
|
11514
|
+
([name, value]) => ` ${name}: ${value === null ? "drop the override (role default applies)" : String(value)}`
|
|
11515
|
+
)
|
|
11516
|
+
);
|
|
11517
|
+
}
|
|
11518
|
+
lines.push(
|
|
11519
|
+
overrides.length ? "overrides:" : "overrides: (none \u2014 every permission follows the role default)",
|
|
11520
|
+
...overrides.map(([name, value]) => ` ${name}: ${String(value)}`)
|
|
11521
|
+
);
|
|
11522
|
+
lines.push(
|
|
11523
|
+
result?.permissions ? `effective permissions: ${Object.entries(result.permissions).filter(([, granted]) => granted).map(([name]) => name).join(", ") || "(none)"}` : "effective permissions: not reported by this Foundry version"
|
|
11524
|
+
);
|
|
11525
|
+
return lines.join("\n");
|
|
11526
|
+
}
|
|
9687
11527
|
function renderUserDetails(user) {
|
|
9688
11528
|
return [
|
|
9689
11529
|
`user: ${user?.name} [${user?.id}]`,
|
|
@@ -9779,6 +11619,28 @@ var RENDERERS = registerRenderers([
|
|
|
9779
11619
|
`commands: ${(result.commands ?? []).join(", ")}`
|
|
9780
11620
|
].join("\n")
|
|
9781
11621
|
),
|
|
11622
|
+
detailRenderer(
|
|
11623
|
+
"game.pause",
|
|
11624
|
+
(result) => [
|
|
11625
|
+
`${result.dryRun ? "[dry-run] would set" : "Set"} the game to ${result.paused ? "PAUSED" : "RUNNING"} for every client`,
|
|
11626
|
+
`previous: ${result.previousPaused ? "paused" : "running"}`,
|
|
11627
|
+
`changed: ${Boolean(result.changed)}`
|
|
11628
|
+
].join("\n")
|
|
11629
|
+
),
|
|
11630
|
+
detailRenderer(
|
|
11631
|
+
"image.show",
|
|
11632
|
+
(result) => [
|
|
11633
|
+
`Showed image ${result.src}${result.title ? ` as "${result.title}"` : ""} to other users`,
|
|
11634
|
+
...renderBroadcastLines(result)
|
|
11635
|
+
].join("\n")
|
|
11636
|
+
),
|
|
11637
|
+
detailRenderer(
|
|
11638
|
+
"system.reload",
|
|
11639
|
+
() => [
|
|
11640
|
+
"Reload requested: the GM browser page reloads as soon as this response is flushed.",
|
|
11641
|
+
"The bridge connection drops and reconnects on its own; retry the next command after a moment."
|
|
11642
|
+
].join("\n")
|
|
11643
|
+
),
|
|
9782
11644
|
customRenderer("world.search", (result, offset) => {
|
|
9783
11645
|
const results = result.results ?? [];
|
|
9784
11646
|
const lines = results.length ? [
|
|
@@ -9852,6 +11714,8 @@ var RENDERERS = registerRenderers([
|
|
|
9852
11714
|
heading: "Scenes",
|
|
9853
11715
|
rows: (scene) => [`${scene.id} ${scene.name}`, ...batchOwnershipLines(scene)]
|
|
9854
11716
|
}),
|
|
11717
|
+
detailRenderer("scene.activate", (result) => renderSceneActivateResult(result)),
|
|
11718
|
+
detailRenderer("scene.pull-users", (result) => renderScenePullUsersResult(result)),
|
|
9855
11719
|
deleteRenderer("scene.delete", "scene", {
|
|
9856
11720
|
suffix: (result) => result.wasActive ? " (was active)" : ""
|
|
9857
11721
|
}),
|
|
@@ -9901,7 +11765,10 @@ var RENDERERS = registerRenderers([
|
|
|
9901
11765
|
heading: "Users",
|
|
9902
11766
|
row: (user) => renderUserSummaryLine(user)
|
|
9903
11767
|
}),
|
|
9904
|
-
detailRenderer("user.get", (result) => renderUserDetails(result.user)),
|
|
11768
|
+
detailRenderer(["user.get", "user.create", "user.update"], (result) => renderUserDetails(result.user)),
|
|
11769
|
+
deleteRenderer("user.delete", "user"),
|
|
11770
|
+
detailRenderer("user.role.set", (result) => renderUserRoleSetResult(result)),
|
|
11771
|
+
detailRenderer("user.permissions.set", (result) => renderUserPermissionsSetResult(result)),
|
|
9905
11772
|
listRenderer("setting.list", {
|
|
9906
11773
|
key: "settings",
|
|
9907
11774
|
empty: "No registered settings found.",
|
|
@@ -9909,6 +11776,13 @@ var RENDERERS = registerRenderers([
|
|
|
9909
11776
|
row: (setting) => renderSettingSummaryLine(setting)
|
|
9910
11777
|
}),
|
|
9911
11778
|
detailRenderer("setting.get", (result) => renderSettingDetails(result.setting)),
|
|
11779
|
+
getManyRenderer("setting.get-many", {
|
|
11780
|
+
key: "settings",
|
|
11781
|
+
heading: "Settings",
|
|
11782
|
+
rows: (setting) => renderSettingBatchRows(setting)
|
|
11783
|
+
}),
|
|
11784
|
+
detailRenderer("setting.set", (result) => renderSettingWriteResult(result)),
|
|
11785
|
+
detailRenderer("setting.set-many", (result) => renderSettingWriteOutcomes(result)),
|
|
9912
11786
|
detailRenderer("actor.ownership.set", (result) => renderOwnershipSetResult("actor", result.actor)),
|
|
9913
11787
|
detailRenderer("item.ownership.set", (result) => renderOwnershipSetResult("item", result.item)),
|
|
9914
11788
|
detailRenderer("scene.ownership.set", (result) => renderOwnershipSetResult("scene", result.scene)),
|
|
@@ -10188,6 +12062,7 @@ var RENDERERS = registerRenderers([
|
|
|
10188
12062
|
}
|
|
10189
12063
|
}),
|
|
10190
12064
|
deleteRenderer("journal.delete", "journal"),
|
|
12065
|
+
detailRenderer("journal.show", (result) => renderJournalShowResult(result)),
|
|
10191
12066
|
listRenderer("journal.category.list", {
|
|
10192
12067
|
key: "categories",
|
|
10193
12068
|
empty: "No categories found.",
|
|
@@ -10233,6 +12108,7 @@ var RENDERERS = registerRenderers([
|
|
|
10233
12108
|
rows: (m) => [`${m.id} ${m.name} ${m.type ?? ""} ${m.scope ?? ""}`, ...batchOwnershipLines(m)]
|
|
10234
12109
|
}),
|
|
10235
12110
|
deleteRenderer("macro.delete", "macro"),
|
|
12111
|
+
detailRenderer("macro.execute", (result) => renderMacroExecuteResult(result)),
|
|
10236
12112
|
listRenderer("playlist.list", {
|
|
10237
12113
|
key: "playlists",
|
|
10238
12114
|
empty: "No playlists found.",
|
|
@@ -10291,6 +12167,7 @@ var RENDERERS = registerRenderers([
|
|
|
10291
12167
|
}),
|
|
10292
12168
|
detailRenderer(["chat.get", "chat.create"], (result) => renderChatDetails(result.message)),
|
|
10293
12169
|
deleteRenderer("chat.delete", "chat message"),
|
|
12170
|
+
detailRenderer("chat.flush", (result) => renderChatFlushResult(result)),
|
|
10294
12171
|
listRenderer("actor.list", {
|
|
10295
12172
|
key: "actors",
|
|
10296
12173
|
empty: "No actors found.",
|
|
@@ -10604,6 +12481,17 @@ var RENDERERS = registerRenderers([
|
|
|
10604
12481
|
],
|
|
10605
12482
|
(result) => renderRegionBehaviorDetails(result.sceneId, result.regionId, result.behavior)
|
|
10606
12483
|
),
|
|
12484
|
+
detailRenderer(
|
|
12485
|
+
[
|
|
12486
|
+
"scene.region.behavior.executable.create",
|
|
12487
|
+
"scene.region.behavior.executable.update",
|
|
12488
|
+
"scene.region.behavior.executable.clone"
|
|
12489
|
+
],
|
|
12490
|
+
(result) => [
|
|
12491
|
+
renderRegionBehaviorDetails(result.sceneId, result.regionId, result.behavior),
|
|
12492
|
+
"note: this behavior RUNS A MACRO when the region fires; system.everyone true runs it in every connected client"
|
|
12493
|
+
].join("\n")
|
|
12494
|
+
),
|
|
10607
12495
|
detailRenderer(
|
|
10608
12496
|
"scene.region.behavior.delete",
|
|
10609
12497
|
(result) => result.deleted ? `Deleted region behavior ${result.id} (scene: ${result.sceneId}, region: ${result.regionId})` : `Would delete region behavior ${result.id} (scene: ${result.sceneId}, region: ${result.regionId})`
|
|
@@ -10692,40 +12580,11 @@ function humanizeCommandResult(command, result, offset = 0) {
|
|
|
10692
12580
|
const renderer = RENDERERS[command];
|
|
10693
12581
|
return renderer ? renderer(result, offset) : JSON.stringify(result, null, 2);
|
|
10694
12582
|
}
|
|
10695
|
-
|
|
10696
|
-
// src/exec.ts
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
"invalid_json",
|
|
10701
|
-
"unexpected_type",
|
|
10702
|
-
"closed",
|
|
10703
|
-
"connect_error"
|
|
10704
|
-
]);
|
|
10705
|
-
function normalizeTransportMessage(message) {
|
|
10706
|
-
if (/ECONNREFUSED/.test(message)) {
|
|
10707
|
-
return "Could not connect to the daemon (connection refused). Is `fvtt-world-cli bridge serve` running?";
|
|
10708
|
-
}
|
|
10709
|
-
if (/ETIMEDOUT/.test(message)) {
|
|
10710
|
-
return "Could not connect to the daemon (connection timed out).";
|
|
10711
|
-
}
|
|
10712
|
-
if (/ENOTFOUND|EAI_AGAIN/.test(message)) {
|
|
10713
|
-
return "Could not resolve the daemon host.";
|
|
10714
|
-
}
|
|
10715
|
-
if (/ECONNRESET/.test(message)) {
|
|
10716
|
-
return "The daemon connection was reset.";
|
|
10717
|
-
}
|
|
10718
|
-
return message;
|
|
10719
|
-
}
|
|
10720
|
-
function renderProtocolError(error) {
|
|
10721
|
-
const code = error?.code ?? "UNKNOWN_ERROR";
|
|
10722
|
-
const message = normalizeTransportMessage(String(error?.message ?? "Unknown error"));
|
|
10723
|
-
const reason = error?.details?.reason;
|
|
10724
|
-
const showDetails = Boolean(error?.details) && !(typeof reason === "string" && TRANSPORT_DETAIL_REASONS.has(reason));
|
|
10725
|
-
const details = showDetails ? `
|
|
10726
|
-
${JSON.stringify(error.details, null, 2)}` : "";
|
|
10727
|
-
return `${code}: ${message}${details}`;
|
|
10728
|
-
}
|
|
12583
|
+
|
|
12584
|
+
// src/exec.ts
|
|
12585
|
+
function isApprovalPending(response) {
|
|
12586
|
+
return !response.ok && response.error?.code === ERROR_CODES.APPROVAL_PENDING;
|
|
12587
|
+
}
|
|
10729
12588
|
async function executeRemoteCommand({
|
|
10730
12589
|
commandName,
|
|
10731
12590
|
params,
|
|
@@ -10752,6 +12611,22 @@ async function executeRemoteCommand({
|
|
|
10752
12611
|
} catch (error) {
|
|
10753
12612
|
response = toTransportErrorEnvelope(error);
|
|
10754
12613
|
}
|
|
12614
|
+
if (isApprovalPending(response)) {
|
|
12615
|
+
response = await awaitApprovalOutcome({
|
|
12616
|
+
pendingResponse: response,
|
|
12617
|
+
send: ({ command: awaitCommand, params: awaitParams, timeoutMs: awaitTimeoutMs, signal }) => dependencies.sendCommand({
|
|
12618
|
+
daemonUrl: clientConfig.daemonUrl,
|
|
12619
|
+
deviceCredential: clientConfig.deviceCredential,
|
|
12620
|
+
command: awaitCommand,
|
|
12621
|
+
params: awaitParams,
|
|
12622
|
+
...clientMaxPayloadOption(dependencies),
|
|
12623
|
+
timeoutMs: awaitTimeoutMs,
|
|
12624
|
+
...signal ? { signal } : {}
|
|
12625
|
+
}),
|
|
12626
|
+
stderr: dependencies.stderr,
|
|
12627
|
+
...typeof timeoutMs === "number" ? { timeoutMs } : {}
|
|
12628
|
+
});
|
|
12629
|
+
}
|
|
10755
12630
|
if (outputJson) {
|
|
10756
12631
|
write(dependencies.stdout, `${JSON.stringify(response, null, 2)}
|
|
10757
12632
|
`);
|
|
@@ -10774,7 +12649,8 @@ ${humanOutput}` : humanOutput;
|
|
|
10774
12649
|
);
|
|
10775
12650
|
}
|
|
10776
12651
|
}
|
|
10777
|
-
async function processExecLine(
|
|
12652
|
+
async function processExecLine(session, rawLine, dryRun) {
|
|
12653
|
+
const timeoutMs = session.timeoutMs;
|
|
10778
12654
|
let parsedLine;
|
|
10779
12655
|
try {
|
|
10780
12656
|
parsedLine = JSON.parse(rawLine);
|
|
@@ -10818,16 +12694,30 @@ async function processExecLine(client, rawLine, timeoutMs, dryRun) {
|
|
|
10818
12694
|
};
|
|
10819
12695
|
}
|
|
10820
12696
|
const requestParams = dryRun && isWriteCommand(obj.command) ? { ...params, dryRun: true } : params;
|
|
12697
|
+
let envelope;
|
|
10821
12698
|
try {
|
|
10822
|
-
|
|
12699
|
+
envelope = await session.send({
|
|
10823
12700
|
command: obj.command,
|
|
10824
12701
|
params: requestParams,
|
|
10825
12702
|
...typeof timeoutMs === "number" ? { timeoutMs } : {}
|
|
10826
12703
|
});
|
|
10827
|
-
return { id: callerId, envelope };
|
|
10828
12704
|
} catch (error) {
|
|
10829
12705
|
return { id: callerId, envelope: toTransportErrorEnvelope(error) };
|
|
10830
12706
|
}
|
|
12707
|
+
if (!isApprovalPending(envelope)) {
|
|
12708
|
+
return { id: callerId, envelope };
|
|
12709
|
+
}
|
|
12710
|
+
return {
|
|
12711
|
+
id: callerId,
|
|
12712
|
+
envelope: await awaitApprovalOutcome({
|
|
12713
|
+
pendingResponse: envelope,
|
|
12714
|
+
send: (request) => session.send(request),
|
|
12715
|
+
reconnect: session.reconnect,
|
|
12716
|
+
onCancelRequested: session.onCancelRequested,
|
|
12717
|
+
stderr: session.stderr,
|
|
12718
|
+
...typeof timeoutMs === "number" ? { timeoutMs } : {}
|
|
12719
|
+
})
|
|
12720
|
+
};
|
|
10831
12721
|
}
|
|
10832
12722
|
async function runExecBatch({
|
|
10833
12723
|
command,
|
|
@@ -10846,17 +12736,33 @@ async function runExecBatch({
|
|
|
10846
12736
|
const timeoutMs = typeof globalOptions.timeoutMs === "number" ? globalOptions.timeoutMs : void 0;
|
|
10847
12737
|
const dryRun = Boolean(globalOptions.dryRun);
|
|
10848
12738
|
const stopOnError = Boolean(command.opts().stopOnError);
|
|
12739
|
+
const connectOptions = {
|
|
12740
|
+
daemonUrl: clientConfig.daemonUrl,
|
|
12741
|
+
deviceCredential: clientConfig.deviceCredential,
|
|
12742
|
+
...clientMaxPayloadOption(dependencies),
|
|
12743
|
+
...typeof timeoutMs === "number" ? { timeoutMs } : {}
|
|
12744
|
+
};
|
|
10849
12745
|
let client;
|
|
10850
12746
|
try {
|
|
10851
|
-
client = await connectDaemonClient(
|
|
10852
|
-
daemonUrl: clientConfig.daemonUrl,
|
|
10853
|
-
deviceCredential: clientConfig.deviceCredential,
|
|
10854
|
-
...clientMaxPayloadOption(dependencies),
|
|
10855
|
-
...typeof timeoutMs === "number" ? { timeoutMs } : {}
|
|
10856
|
-
});
|
|
12747
|
+
client = await connectDaemonClient(connectOptions);
|
|
10857
12748
|
} catch (error) {
|
|
10858
12749
|
throw new ExecConnectError(toTransportErrorEnvelope(error));
|
|
10859
12750
|
}
|
|
12751
|
+
let cancelRequested = false;
|
|
12752
|
+
const session = {
|
|
12753
|
+
send: (request) => client.send(request),
|
|
12754
|
+
reconnect: async () => {
|
|
12755
|
+
const replaced = client;
|
|
12756
|
+
client = await connectDaemonClient(connectOptions);
|
|
12757
|
+
void replaced.close().catch(() => {
|
|
12758
|
+
});
|
|
12759
|
+
},
|
|
12760
|
+
stderr: dependencies.stderr,
|
|
12761
|
+
...typeof timeoutMs === "number" ? { timeoutMs } : {},
|
|
12762
|
+
onCancelRequested: () => {
|
|
12763
|
+
cancelRequested = true;
|
|
12764
|
+
}
|
|
12765
|
+
};
|
|
10860
12766
|
const rl = createInterface({ input: stdin, crlfDelay: Infinity });
|
|
10861
12767
|
let anyFailed = false;
|
|
10862
12768
|
let index = 0;
|
|
@@ -10867,7 +12773,7 @@ async function runExecBatch({
|
|
|
10867
12773
|
if (rawLine.trim() === "") {
|
|
10868
12774
|
continue;
|
|
10869
12775
|
}
|
|
10870
|
-
const { id, envelope } = await processExecLine(
|
|
12776
|
+
const { id, envelope } = await processExecLine(session, rawLine, dryRun);
|
|
10871
12777
|
const outputLine = { ...envelope, index: currentIndex, ...id !== void 0 ? { id } : {} };
|
|
10872
12778
|
write(dependencies.stdout, `${JSON.stringify(outputLine)}
|
|
10873
12779
|
`);
|
|
@@ -10877,6 +12783,10 @@ async function runExecBatch({
|
|
|
10877
12783
|
break;
|
|
10878
12784
|
}
|
|
10879
12785
|
}
|
|
12786
|
+
if (cancelRequested) {
|
|
12787
|
+
anyFailed = true;
|
|
12788
|
+
break;
|
|
12789
|
+
}
|
|
10880
12790
|
}
|
|
10881
12791
|
} finally {
|
|
10882
12792
|
rl.close();
|
|
@@ -11090,6 +13000,73 @@ var REGION_BEHAVIOR_FIELDS = [
|
|
|
11090
13000
|
function addRegionBehaviorFieldOptions(command, verb) {
|
|
11091
13001
|
return addFieldOptions(command, verb, REGION_BEHAVIOR_FIELDS);
|
|
11092
13002
|
}
|
|
13003
|
+
var EXECUTABLE_BEHAVIOR_FIELDS = [
|
|
13004
|
+
...REGION_BEHAVIOR_FIELDS,
|
|
13005
|
+
{
|
|
13006
|
+
flag: "--macro-uuid <uuid>",
|
|
13007
|
+
help: {
|
|
13008
|
+
create: "UUID of the world macro to run (e.g. Macro.abc123); a compendium macro is refused",
|
|
13009
|
+
update: "UUID of the world macro to run instead",
|
|
13010
|
+
clone: "UUID of the world macro the clone runs"
|
|
13011
|
+
}
|
|
13012
|
+
},
|
|
13013
|
+
{
|
|
13014
|
+
flag: "--events <list>",
|
|
13015
|
+
help: {
|
|
13016
|
+
create: "Comma-separated region events that run the macro (e.g. tokenEnter,tokenExit)",
|
|
13017
|
+
update: "Comma-separated region events that run the macro",
|
|
13018
|
+
clone: "Comma-separated region events for the clone"
|
|
13019
|
+
},
|
|
13020
|
+
parser: (value) => parseCsvList(value, "--events")
|
|
13021
|
+
},
|
|
13022
|
+
{
|
|
13023
|
+
flag: "--everyone <everyone>",
|
|
13024
|
+
help: {
|
|
13025
|
+
create: "Run the macro on EVERY connected client instead of one elected executor (true|false)",
|
|
13026
|
+
update: "Run the macro on EVERY connected client instead of one elected executor (true|false)",
|
|
13027
|
+
clone: "Run the macro on EVERY connected client instead of one elected executor (true|false)"
|
|
13028
|
+
},
|
|
13029
|
+
parser: parseBoolean
|
|
13030
|
+
}
|
|
13031
|
+
];
|
|
13032
|
+
function addExecutableBehaviorFieldOptions(command, verb) {
|
|
13033
|
+
return addFieldOptions(command, verb, EXECUTABLE_BEHAVIOR_FIELDS);
|
|
13034
|
+
}
|
|
13035
|
+
var USER_FIELDS = [
|
|
13036
|
+
{
|
|
13037
|
+
flag: "--color <color>",
|
|
13038
|
+
help: { create: "User color (e.g. #ff0000)", update: "New user color", clone: "User color override" }
|
|
13039
|
+
},
|
|
13040
|
+
{
|
|
13041
|
+
flag: "--pronouns <pronouns>",
|
|
13042
|
+
help: { create: "Pronouns shown to other players", update: "New pronouns", clone: "Pronouns override" }
|
|
13043
|
+
},
|
|
13044
|
+
{
|
|
13045
|
+
flag: "--avatar <avatar>",
|
|
13046
|
+
help: { create: "Avatar image path", update: "New avatar image path", clone: "Avatar override" }
|
|
13047
|
+
},
|
|
13048
|
+
{
|
|
13049
|
+
flag: "--clear-avatar",
|
|
13050
|
+
help: "Explicitly set avatar to null",
|
|
13051
|
+
conflicts: "avatar"
|
|
13052
|
+
},
|
|
13053
|
+
{
|
|
13054
|
+
flag: "--character <actorId>",
|
|
13055
|
+
help: {
|
|
13056
|
+
create: "Actor id of the assigned character",
|
|
13057
|
+
update: "Actor id of the assigned character",
|
|
13058
|
+
clone: "Assigned character override"
|
|
13059
|
+
}
|
|
13060
|
+
},
|
|
13061
|
+
{
|
|
13062
|
+
flag: "--clear-character",
|
|
13063
|
+
help: "Explicitly unassign the character (character = null)",
|
|
13064
|
+
conflicts: "character"
|
|
13065
|
+
}
|
|
13066
|
+
];
|
|
13067
|
+
function addUserFieldOptions(command, verb) {
|
|
13068
|
+
return addFieldOptions(command, verb, USER_FIELDS);
|
|
13069
|
+
}
|
|
11093
13070
|
function effectFields(transferNote) {
|
|
11094
13071
|
return [
|
|
11095
13072
|
{
|
|
@@ -12384,8 +14361,7 @@ function createDaemonControlRunner(dependencies) {
|
|
|
12384
14361
|
}
|
|
12385
14362
|
|
|
12386
14363
|
// src/commands/auth.ts
|
|
12387
|
-
var AWAIT_CLIENT_TIMEOUT_FLOOR_MS = AUTH_AWAIT_PARK_CAP_MS +
|
|
12388
|
-
var AWAIT_EMPTY_POLL_DELAY_MS = 250;
|
|
14364
|
+
var AWAIT_CLIENT_TIMEOUT_FLOOR_MS = AUTH_AWAIT_PARK_CAP_MS + AWAIT_FLOOR_MARGIN_MS;
|
|
12389
14365
|
var INTERACTIVE_WAIT_GUIDANCE = "Waiting for a pairing request needs an interactive terminal. Use auth pending and auth approve --yes instead.";
|
|
12390
14366
|
var PRUNE_INTERACTIVE_GUIDANCE = "Non-interactive pairing prune requires --yes";
|
|
12391
14367
|
var PRUNE_JSON_GUIDANCE = "Pairing prune with --json requires --yes, because the confirmation prompt is never mixed into JSON output";
|
|
@@ -12723,10 +14699,12 @@ function syncInstalledSkillCopies(dependencies) {
|
|
|
12723
14699
|
try {
|
|
12724
14700
|
const sourceDirectory = resolveSkillSourceDirectory();
|
|
12725
14701
|
if (!sourceDirectory) return;
|
|
14702
|
+
let installedAnywhere = false;
|
|
12726
14703
|
for (const root of knownSkillRoots(dependencies)) {
|
|
12727
14704
|
try {
|
|
12728
14705
|
const destination = join2(root, SKILL_NAME);
|
|
12729
14706
|
const state = inspectInstalledSkill(destination, sourceDirectory);
|
|
14707
|
+
if (state !== "missing") installedAnywhere = true;
|
|
12730
14708
|
if (state === "pristine") {
|
|
12731
14709
|
rmSync(destination, { recursive: true, force: true });
|
|
12732
14710
|
cpSync(sourceDirectory, destination, { recursive: true });
|
|
@@ -12746,6 +14724,13 @@ function syncInstalledSkillCopies(dependencies) {
|
|
|
12746
14724
|
continue;
|
|
12747
14725
|
}
|
|
12748
14726
|
}
|
|
14727
|
+
if (!installedAnywhere) {
|
|
14728
|
+
write(
|
|
14729
|
+
dependencies.stderr,
|
|
14730
|
+
`The ${SKILL_NAME} agent skill is not installed. If AI agents use this CLI, consider running \`fvtt-world-cli skill install\` to give them its operating manual.
|
|
14731
|
+
`
|
|
14732
|
+
);
|
|
14733
|
+
}
|
|
12749
14734
|
} catch {
|
|
12750
14735
|
return;
|
|
12751
14736
|
}
|
|
@@ -13155,6 +15140,16 @@ function registerChat({ program, dependencies }) {
|
|
|
13155
15140
|
dependencies
|
|
13156
15141
|
});
|
|
13157
15142
|
});
|
|
15143
|
+
chat.command("flush").description(
|
|
15144
|
+
"Delete the ENTIRE chat log (irreversible). --dry-run reports the message count without deleting anything"
|
|
15145
|
+
).action(async function flushChat() {
|
|
15146
|
+
await executeRemoteCommand({
|
|
15147
|
+
commandName: "chat.flush",
|
|
15148
|
+
params: {},
|
|
15149
|
+
command: this,
|
|
15150
|
+
dependencies
|
|
15151
|
+
});
|
|
15152
|
+
});
|
|
13158
15153
|
chat.command("delete").requiredOption("--message-id <messageId>", "Chat message id").action(async function deleteChat(options) {
|
|
13159
15154
|
await executeRemoteCommand({
|
|
13160
15155
|
commandName: "chat.delete",
|
|
@@ -13606,20 +15601,93 @@ function listDocEntries(docsDirectory) {
|
|
|
13606
15601
|
}
|
|
13607
15602
|
|
|
13608
15603
|
// src/commands/discovery.ts
|
|
15604
|
+
var POLICY_AVAILABILITY_CODES = /* @__PURE__ */ new Set([
|
|
15605
|
+
ERROR_CODES.DAEMON_UNAVAILABLE,
|
|
15606
|
+
ERROR_CODES.BRIDGE_NOT_READY,
|
|
15607
|
+
ERROR_CODES.BRIDGE_DISCONNECTED
|
|
15608
|
+
]);
|
|
15609
|
+
function commandNameSet(value) {
|
|
15610
|
+
return new Set(
|
|
15611
|
+
Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : []
|
|
15612
|
+
);
|
|
15613
|
+
}
|
|
15614
|
+
async function discoverPolicy(command, dependencies) {
|
|
15615
|
+
const clientConfig = getCommandConfig(command, dependencies);
|
|
15616
|
+
let response;
|
|
15617
|
+
try {
|
|
15618
|
+
response = await dependencies.sendCommand({
|
|
15619
|
+
daemonUrl: clientConfig.daemonUrl,
|
|
15620
|
+
deviceCredential: clientConfig.deviceCredential,
|
|
15621
|
+
command: POLICY_SNAPSHOT_COMMAND,
|
|
15622
|
+
params: {},
|
|
15623
|
+
...clientMaxPayloadOption(dependencies),
|
|
15624
|
+
timeoutMs: POLICY_DISCOVERY_TIMEOUT_MS
|
|
15625
|
+
});
|
|
15626
|
+
} catch (error) {
|
|
15627
|
+
response = toTransportErrorEnvelope(error);
|
|
15628
|
+
}
|
|
15629
|
+
if (response.ok) {
|
|
15630
|
+
const snapshot = response.result ?? {};
|
|
15631
|
+
return {
|
|
15632
|
+
status: "applied",
|
|
15633
|
+
approve: commandNameSet(snapshot.approve),
|
|
15634
|
+
deny: commandNameSet(snapshot.deny)
|
|
15635
|
+
};
|
|
15636
|
+
}
|
|
15637
|
+
const code = response.error?.code ?? ERROR_CODES.INTERNAL_ERROR;
|
|
15638
|
+
if (!POLICY_AVAILABILITY_CODES.has(code)) {
|
|
15639
|
+
return { status: "failed", envelope: response };
|
|
15640
|
+
}
|
|
15641
|
+
return { status: "fallback", reason: code, detail: response.error?.message ?? "no reason reported" };
|
|
15642
|
+
}
|
|
13609
15643
|
function registerDiscovery({ program, dependencies }) {
|
|
13610
|
-
program.command("commands").description("List
|
|
13611
|
-
const
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
15644
|
+
program.command("commands").description("List discoverable protocol commands and whether each mutates world state").action(async function listCommands() {
|
|
15645
|
+
const json = Boolean(this.optsWithGlobals().json);
|
|
15646
|
+
const discovery = await discoverPolicy(this, dependencies);
|
|
15647
|
+
if (discovery.status === "failed") {
|
|
15648
|
+
const error = discovery.envelope.error;
|
|
15649
|
+
if (json) {
|
|
15650
|
+
write(dependencies.stdout, `${JSON.stringify(discovery.envelope, null, 2)}
|
|
15651
|
+
`);
|
|
15652
|
+
} else {
|
|
15653
|
+
write(dependencies.stderr, `${renderProtocolError(error)}
|
|
13617
15654
|
`);
|
|
15655
|
+
}
|
|
15656
|
+
throw new CommanderError7(
|
|
15657
|
+
exitCodeForErrorCode(error?.code),
|
|
15658
|
+
"fvtt-world-cli.remoteError",
|
|
15659
|
+
error?.message ?? "Reading the command policy failed"
|
|
15660
|
+
);
|
|
15661
|
+
}
|
|
15662
|
+
const applied = discovery.status === "applied";
|
|
15663
|
+
const payload = DISCOVERABLE_COMMAND_NAMES.filter((name) => !applied || !discovery.deny.has(name)).map(
|
|
15664
|
+
(name) => ({
|
|
15665
|
+
command: name,
|
|
15666
|
+
mutation: COMMAND_DEFINITIONS[name].mutation,
|
|
15667
|
+
...applied && discovery.approve.has(name) ? { approval: true } : {}
|
|
15668
|
+
})
|
|
15669
|
+
);
|
|
15670
|
+
const policy = applied ? { applied: true, source: "bridge" } : { applied: false, source: "static", reason: discovery.reason };
|
|
15671
|
+
if (json) {
|
|
15672
|
+
write(
|
|
15673
|
+
dependencies.stdout,
|
|
15674
|
+
`${JSON.stringify({ ...localSuccessEnvelope(payload), policy }, null, 2)}
|
|
15675
|
+
`
|
|
15676
|
+
);
|
|
13618
15677
|
return;
|
|
13619
15678
|
}
|
|
15679
|
+
if (!applied) {
|
|
15680
|
+
write(
|
|
15681
|
+
dependencies.stderr,
|
|
15682
|
+
`warning: the command policy of the GM client holding the bridge could not be read (${discovery.reason}: ${discovery.detail}); this is the full static command registry, so denied commands are still listed and commands needing approval are not marked.
|
|
15683
|
+
`
|
|
15684
|
+
);
|
|
15685
|
+
}
|
|
13620
15686
|
write(
|
|
13621
15687
|
dependencies.stdout,
|
|
13622
|
-
`${payload.map(
|
|
15688
|
+
`${payload.map(
|
|
15689
|
+
(entry) => `${entry.command} ${entry.mutation ? "write" : "read"}${entry.approval ? " approval" : ""}`
|
|
15690
|
+
).join("\n")}
|
|
13623
15691
|
`
|
|
13624
15692
|
);
|
|
13625
15693
|
});
|
|
@@ -13891,6 +15959,56 @@ function registerFolder({ program, dependencies }) {
|
|
|
13891
15959
|
});
|
|
13892
15960
|
}
|
|
13893
15961
|
|
|
15962
|
+
// src/commands/game.ts
|
|
15963
|
+
function registerGame({ program, dependencies }) {
|
|
15964
|
+
const game = program.command("game").description("Foundry game-state commands");
|
|
15965
|
+
game.addHelpText(
|
|
15966
|
+
"after",
|
|
15967
|
+
"\nResult key (--json): .result.paused, .result.previousPaused and .result.changed (pause)."
|
|
15968
|
+
);
|
|
15969
|
+
game.command("pause").description("Pause or unpause the game for every connected client").requiredOption("--paused <paused>", "Target pause state (true|false)", parseBoolean).action(async function pauseGame(options) {
|
|
15970
|
+
await executeRemoteCommand({
|
|
15971
|
+
commandName: "game.pause",
|
|
15972
|
+
params: { paused: options.paused },
|
|
15973
|
+
command: this,
|
|
15974
|
+
dependencies
|
|
15975
|
+
});
|
|
15976
|
+
});
|
|
15977
|
+
}
|
|
15978
|
+
|
|
15979
|
+
// src/commands/image.ts
|
|
15980
|
+
function registerImage({ program, dependencies }) {
|
|
15981
|
+
const image = program.command("image").description("Foundry image popout commands");
|
|
15982
|
+
image.addHelpText(
|
|
15983
|
+
"after",
|
|
15984
|
+
[
|
|
15985
|
+
"",
|
|
15986
|
+
"Result key (--json): .result.src, .result.title, .result.userIds, .result.activeUserIds,",
|
|
15987
|
+
".result.inactiveUserIds and .result.dispatched (show).",
|
|
15988
|
+
"",
|
|
15989
|
+
"`--src` takes a path inside Foundry's managed `data` source (e.g. worlds/<worldId>/art/map.webp)",
|
|
15990
|
+
"or an http(s) URL. A host filesystem path is refused. Showing an image is a broadcast, not a",
|
|
15991
|
+
"mutation: nothing is written and nothing can be confirmed afterwards."
|
|
15992
|
+
].join("\n")
|
|
15993
|
+
);
|
|
15994
|
+
image.command("show").description("Pop an image out in other users' browsers").requiredOption("--src <src>", "Managed data-relative image path or http(s) URL").option("--title <title>", "Window title (default: derived by Foundry from the source)").option(
|
|
15995
|
+
"--user-ids <list>",
|
|
15996
|
+
"Comma-separated user ids to show it to (default: every user); offline users are reported as skipped",
|
|
15997
|
+
(value) => parseCsvList(value, "--user-ids")
|
|
15998
|
+
).action(async function showImage(options) {
|
|
15999
|
+
await executeRemoteCommand({
|
|
16000
|
+
commandName: "image.show",
|
|
16001
|
+
params: {
|
|
16002
|
+
src: options.src,
|
|
16003
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
16004
|
+
...options.userIds ? { userIds: options.userIds } : {}
|
|
16005
|
+
},
|
|
16006
|
+
command: this,
|
|
16007
|
+
dependencies
|
|
16008
|
+
});
|
|
16009
|
+
});
|
|
16010
|
+
}
|
|
16011
|
+
|
|
13894
16012
|
// src/commands/item.ts
|
|
13895
16013
|
function registerItem({ program, dependencies }) {
|
|
13896
16014
|
const { registerOwnershipSet, registerCompendiumImport, registerBatchWriteCommands } = createSharedRegistrars(dependencies);
|
|
@@ -14228,6 +16346,27 @@ delete does NOT re-point pages (Foundry rewrites nothing either); it reports how
|
|
|
14228
16346
|
dependencies
|
|
14229
16347
|
});
|
|
14230
16348
|
});
|
|
16349
|
+
journal.command("show").description("Open a journal entry in other users' browsers (a broadcast, not a mutation)").requiredOption("--journal-id <journalId>", "Journal entry id").addOption(
|
|
16350
|
+
new Option8(
|
|
16351
|
+
"--force <force>",
|
|
16352
|
+
"Push the entry even to users who cannot normally see it (true|false)"
|
|
16353
|
+
).argParser(parseBoolean)
|
|
16354
|
+
).option(
|
|
16355
|
+
"--user-ids <list>",
|
|
16356
|
+
"Comma-separated user ids to show it to (default: every user); offline users are reported as skipped",
|
|
16357
|
+
(value) => parseCsvList(value, "--user-ids")
|
|
16358
|
+
).action(async function showJournal(options) {
|
|
16359
|
+
await executeRemoteCommand({
|
|
16360
|
+
commandName: "journal.show",
|
|
16361
|
+
params: {
|
|
16362
|
+
journalId: options.journalId,
|
|
16363
|
+
...booleanField("force", options.force),
|
|
16364
|
+
...options.userIds ? { userIds: options.userIds } : {}
|
|
16365
|
+
},
|
|
16366
|
+
command: this,
|
|
16367
|
+
dependencies
|
|
16368
|
+
});
|
|
16369
|
+
});
|
|
14231
16370
|
registerCompendiumImport(journal, {
|
|
14232
16371
|
commandName: "journal.import-from-compendium",
|
|
14233
16372
|
noun: "journal entry",
|
|
@@ -14236,6 +16375,7 @@ delete does NOT re-point pages (Foundry rewrites nothing either); it reports how
|
|
|
14236
16375
|
}
|
|
14237
16376
|
|
|
14238
16377
|
// src/commands/macro.ts
|
|
16378
|
+
import { Option as Option9 } from "commander";
|
|
14239
16379
|
function registerMacro({ program, dependencies }) {
|
|
14240
16380
|
const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
|
|
14241
16381
|
const macro = program.command("macro").description("Foundry macro commands");
|
|
@@ -14317,6 +16457,19 @@ function registerMacro({ program, dependencies }) {
|
|
|
14317
16457
|
dependencies
|
|
14318
16458
|
});
|
|
14319
16459
|
});
|
|
16460
|
+
macro.command("execute").description("Run a macro in the GM client and wait for it to finish").requiredOption("--macro-id <macroId>", "Macro id").option("--actor-id <actorId>", "Actor the macro receives as its `actor`/speaker").option("--scene-id <sceneId>", "Scene the token belongs to (required with --token-id)").option("--token-id <tokenId>", "Token the macro receives as its `token` (needs --scene-id)").option("--args-json <json>", "Extra scope values as a JSON object, reachable as `scope` in the macro").addOption(
|
|
16461
|
+
new Option9(
|
|
16462
|
+
"--macro-timeout-ms <ms>",
|
|
16463
|
+
`How long the GM client waits for the macro, in ms (default ${MACRO_EXECUTE_TIMEOUT_DEFAULT_MS}, max ${MACRO_EXECUTE_TIMEOUT_MAX_MS}). Raise the global --timeout-ms above it, or the CLI stops waiting before MACRO_TIMEOUT can be reported.`
|
|
16464
|
+
).argParser(parsePositiveInt)
|
|
16465
|
+
).action(async function executeMacroCommand(options) {
|
|
16466
|
+
await executeRemoteCommand({
|
|
16467
|
+
commandName: "macro.execute",
|
|
16468
|
+
params: createMacroExecuteParams(options),
|
|
16469
|
+
command: this,
|
|
16470
|
+
dependencies
|
|
16471
|
+
});
|
|
16472
|
+
});
|
|
14320
16473
|
registerCompendiumImport(macro, {
|
|
14321
16474
|
commandName: "macro.import-from-compendium",
|
|
14322
16475
|
noun: "macro",
|
|
@@ -14325,7 +16478,7 @@ function registerMacro({ program, dependencies }) {
|
|
|
14325
16478
|
}
|
|
14326
16479
|
|
|
14327
16480
|
// src/commands/playlist.ts
|
|
14328
|
-
import { Option as
|
|
16481
|
+
import { Option as Option10 } from "commander";
|
|
14329
16482
|
function registerPlaylist({ program, dependencies }) {
|
|
14330
16483
|
const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
|
|
14331
16484
|
const playlist = program.command("playlist").description("Foundry playlist commands");
|
|
@@ -14422,7 +16575,7 @@ function registerPlaylist({ program, dependencies }) {
|
|
|
14422
16575
|
dependencies
|
|
14423
16576
|
});
|
|
14424
16577
|
});
|
|
14425
|
-
playlist.command("play-next").requiredOption("--playlist-id <playlistId>", "Playlist id").option("--sound-id <soundId>", "Current sound id to advance from").addOption(new
|
|
16578
|
+
playlist.command("play-next").requiredOption("--playlist-id <playlistId>", "Playlist id").option("--sound-id <soundId>", "Current sound id to advance from").addOption(new Option10("--direction <direction>", "1 (forward) or -1 (backward)").argParser(parseNumber)).action(async function playNextPlaylistCommand(options) {
|
|
14426
16579
|
await executeRemoteCommand({
|
|
14427
16580
|
commandName: "playlist.playNext",
|
|
14428
16581
|
params: {
|
|
@@ -14525,7 +16678,7 @@ function registerPlaylist({ program, dependencies }) {
|
|
|
14525
16678
|
}
|
|
14526
16679
|
|
|
14527
16680
|
// src/commands/scene.ts
|
|
14528
|
-
import { Option as
|
|
16681
|
+
import { Option as Option11 } from "commander";
|
|
14529
16682
|
function registerScene({ program, dependencies }) {
|
|
14530
16683
|
const { registerOwnershipSet, registerCompendiumImport, registerBatchWriteCommands } = createSharedRegistrars(dependencies);
|
|
14531
16684
|
const scene = program.command("scene").description("Foundry scene commands");
|
|
@@ -14604,11 +16757,36 @@ function registerScene({ program, dependencies }) {
|
|
|
14604
16757
|
dependencies
|
|
14605
16758
|
});
|
|
14606
16759
|
});
|
|
16760
|
+
scene.command("activate").description("Make a scene the active one for the whole world (a no-op reports changed:false)").requiredOption("--scene-id <sceneId>", "Scene id").action(async function activateScene(options) {
|
|
16761
|
+
await executeRemoteCommand({
|
|
16762
|
+
commandName: "scene.activate",
|
|
16763
|
+
params: { sceneId: options.sceneId },
|
|
16764
|
+
command: this,
|
|
16765
|
+
dependencies
|
|
16766
|
+
});
|
|
16767
|
+
});
|
|
16768
|
+
scene.command("pull-users").description(
|
|
16769
|
+
"Pull users' views to a scene (a broadcast, not a mutation: only currently ONLINE users are reachable and nothing can be confirmed afterwards)"
|
|
16770
|
+
).requiredOption("--scene-id <sceneId>", "Scene id").option(
|
|
16771
|
+
"--user-ids <list>",
|
|
16772
|
+
"Comma-separated user ids to pull (default: every online user); offline users are reported as skipped",
|
|
16773
|
+
(value) => parseCsvList(value, "--user-ids")
|
|
16774
|
+
).action(async function pullUsersToScene(options) {
|
|
16775
|
+
await executeRemoteCommand({
|
|
16776
|
+
commandName: "scene.pull-users",
|
|
16777
|
+
params: {
|
|
16778
|
+
sceneId: options.sceneId,
|
|
16779
|
+
...options.userIds ? { userIds: options.userIds } : {}
|
|
16780
|
+
},
|
|
16781
|
+
command: this,
|
|
16782
|
+
dependencies
|
|
16783
|
+
});
|
|
16784
|
+
});
|
|
14607
16785
|
const sceneThumbnail = scene.command("thumbnail").description("Scene thumbnail commands");
|
|
14608
16786
|
sceneThumbnail.command("generate").requiredOption("--scene-id <sceneId>", "Scene id").addOption(
|
|
14609
|
-
new
|
|
16787
|
+
new Option11("--width <width>", "Thumbnail width in px (16\u20131024, default 300)").argParser(parseNumber)
|
|
14610
16788
|
).addOption(
|
|
14611
|
-
new
|
|
16789
|
+
new Option11("--height <height>", "Thumbnail height in px (16\u20131024, default 100)").argParser(parseNumber)
|
|
14612
16790
|
).option(
|
|
14613
16791
|
"--include-thumb",
|
|
14614
16792
|
"Include the generated base64 data URL in the response (read it with --json; the SCENE stores the extracted asset path, reported as .thumbnail.storedPath)"
|
|
@@ -15023,7 +17201,7 @@ function registerScene({ program, dependencies }) {
|
|
|
15023
17201
|
});
|
|
15024
17202
|
registerBatchWriteCommands(sceneSound, { prefix: "scene.sound", noun: "sound" });
|
|
15025
17203
|
const sceneWall = scene.command("wall").description("Scene-embedded wall commands");
|
|
15026
|
-
addPaginationOptions(sceneWall.command("list").requiredOption("--scene-id <sceneId>", "Scene id")).addOption(new
|
|
17204
|
+
addPaginationOptions(sceneWall.command("list").requiredOption("--scene-id <sceneId>", "Scene id")).addOption(new Option11("--door", "Return only doors and secret doors (door > 0)")).action(async function listSceneWalls(options) {
|
|
15027
17205
|
await executeRemoteCommand({
|
|
15028
17206
|
commandName: "scene.wall.list",
|
|
15029
17207
|
params: {
|
|
@@ -15351,7 +17529,7 @@ function registerScene({ program, dependencies }) {
|
|
|
15351
17529
|
const sceneRegionBehavior = sceneRegion.command("behavior").description("Region-embedded behavior commands (what a region DOES when it fires)");
|
|
15352
17530
|
sceneRegionBehavior.addHelpText(
|
|
15353
17531
|
"after",
|
|
15354
|
-
|
|
17532
|
+
"\nResult key (--json): .result.behavior (single/write) / .result.behaviors[] (list), with .result.sceneId and .result.regionId alongside.\nA behavior name may legitimately be BLANK (Foundry then displays the localized type label): pass --name \"\" to author one; the CLI prints it as (blank).\n--type is CREATE-ONLY, so update/clone have no --type. If a --patch-json carries a `type` key it is refused even when it restates the current type: drop the key and resend (an unchanged type is a no-op for Foundry). On this family changing a behavior's type is impossible \u2014 for that, create the new behavior and delete the old one.\nThe code-executing types executeScript and executeMacro are REFUSED on THIS family whenever you supply them, and every patch on an existing one is refused too (even --disabled). executeScript is refused everywhere \u2014 it runs its source in every connected player's browser. For executeMacro use `scene region behavior executable`; delete removes either type.\nclone with NO patch flags IS allowed on any behavior \u2014 note that cloning a code-executing one mints a NEW self-arming auto-trigger, so audit the region afterwards."
|
|
15355
17533
|
);
|
|
15356
17534
|
addNameFilterOption(
|
|
15357
17535
|
addPaginationOptions(
|
|
@@ -15422,6 +17600,44 @@ function registerScene({ program, dependencies }) {
|
|
|
15422
17600
|
dependencies
|
|
15423
17601
|
});
|
|
15424
17602
|
});
|
|
17603
|
+
const executableBehavior = sceneRegionBehavior.command("executable").description("Region behaviors that RUN A MACRO when the region fires");
|
|
17604
|
+
executableBehavior.addHelpText(
|
|
17605
|
+
"after",
|
|
17606
|
+
"\nResult key (--json): .result.behavior, with .result.sceneId and .result.regionId alongside \u2014 the same shape the ordinary behavior verbs return.\nThese three verbs are the only route to an executeMacro behavior. The type is fixed: there is no --type, and the behavior cannot be turned into another type later.\n--macro-uuid must name a macro in THIS world (e.g. Macro.abc123); a compendium macro or a missing uuid is refused, so the behavior never arms a trigger that points at nothing.\n--events decides WHEN the macro runs (e.g. tokenEnter), and --everyone true runs it in EVERY connected client instead of one elected executor \u2014 read it as: every player's browser executes that macro.\nclone with no patch flags mints a second armed trigger on the same region; audit the region afterwards."
|
|
17607
|
+
);
|
|
17608
|
+
addExecutableBehaviorFieldOptions(
|
|
17609
|
+
addIdempotencyKeyOption(executableBehavior.command("create")).description("Arm a region with an executeMacro behavior").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id"),
|
|
17610
|
+
"create"
|
|
17611
|
+
).option("--data-json <json>", "Full/extra behavior data as a JSON object (merged last)").action(async function createExecutableBehavior(options) {
|
|
17612
|
+
await executeRemoteCommand({
|
|
17613
|
+
commandName: "scene.region.behavior.executable.create",
|
|
17614
|
+
params: createExecutableBehaviorCreateParams(options),
|
|
17615
|
+
command: this,
|
|
17616
|
+
dependencies
|
|
17617
|
+
});
|
|
17618
|
+
});
|
|
17619
|
+
addExecutableBehaviorFieldOptions(
|
|
17620
|
+
executableBehavior.command("update").description("Edit an executeMacro behavior (the only route the ordinary update refuses)").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id").requiredOption("--behavior-id <behaviorId>", "Behavior id"),
|
|
17621
|
+
"update"
|
|
17622
|
+
).option("--patch-json <json>", "Full/extra behavior patch as a JSON object (merged last)").action(async function updateExecutableBehavior(options) {
|
|
17623
|
+
await executeRemoteCommand({
|
|
17624
|
+
commandName: "scene.region.behavior.executable.update",
|
|
17625
|
+
params: createExecutableBehaviorUpdateParams(options),
|
|
17626
|
+
command: this,
|
|
17627
|
+
dependencies
|
|
17628
|
+
});
|
|
17629
|
+
});
|
|
17630
|
+
addExecutableBehaviorFieldOptions(
|
|
17631
|
+
addIdempotencyKeyOption(executableBehavior.command("clone")).description("Copy an executeMacro behavior into a second armed trigger").requiredOption("--scene-id <sceneId>", "Scene id").requiredOption("--region-id <regionId>", "Region id").requiredOption("--behavior-id <behaviorId>", "Source behavior id"),
|
|
17632
|
+
"clone"
|
|
17633
|
+
).option("--patch-json <json>", "Override fields for the clone as a JSON object (merged last)").action(async function cloneExecutableBehavior(options) {
|
|
17634
|
+
await executeRemoteCommand({
|
|
17635
|
+
commandName: "scene.region.behavior.executable.clone",
|
|
17636
|
+
params: createExecutableBehaviorCloneParams(options),
|
|
17637
|
+
command: this,
|
|
17638
|
+
dependencies
|
|
17639
|
+
});
|
|
17640
|
+
});
|
|
15425
17641
|
registerCompendiumImport(scene, {
|
|
15426
17642
|
commandName: "scene.import-from-compendium",
|
|
15427
17643
|
noun: "scene",
|
|
@@ -15522,19 +17738,27 @@ function registerScene({ program, dependencies }) {
|
|
|
15522
17738
|
}
|
|
15523
17739
|
|
|
15524
17740
|
// src/commands/setting.ts
|
|
17741
|
+
import { Option as Option12 } from "commander";
|
|
15525
17742
|
function registerSetting({ program, dependencies }) {
|
|
15526
|
-
const setting = program.command("setting").description("Foundry game settings
|
|
17743
|
+
const setting = program.command("setting").description("Foundry game settings");
|
|
15527
17744
|
setting.addHelpText(
|
|
15528
17745
|
"after",
|
|
15529
17746
|
[
|
|
15530
17747
|
"",
|
|
15531
|
-
"Result key (--json): .result.settings[] (list, metadata only
|
|
17748
|
+
"Result key (--json): .result.settings[] (list, metadata only; get-many, metadata + value) /",
|
|
17749
|
+
".result.setting (get, metadata + value). A write returns the row's fields directly on .result",
|
|
17750
|
+
"(namespace, key, scope, previous, value, requiresReload, changed) \u2014 set-many returns .result.outcomes[].",
|
|
15532
17751
|
"",
|
|
15533
17752
|
"Only settings REGISTERED IN THIS SESSION are visible: a value still stored in the world by a",
|
|
15534
17753
|
"disabled/uninstalled module has no registration and reports SETTING_NOT_FOUND.",
|
|
15535
17754
|
"`--name` matches the namespace, the key, the raw registration name AND the localized label.",
|
|
15536
17755
|
"Scope caveats: a `client`-scope value is this GM CLIENT's localStorage value, and a `user`-scope",
|
|
15537
|
-
"value is resolved for the CURRENT GM USER \u2014 neither is ever another player's value."
|
|
17756
|
+
"value is resolved for the CURRENT GM USER \u2014 neither is ever another player's value.",
|
|
17757
|
+
"",
|
|
17758
|
+
"`set`/`set-many` always refuse the `fvtt-world-cli` namespace, so the bridge's own",
|
|
17759
|
+
"authorization settings stay beyond the CLI's reach. A write whose row reports",
|
|
17760
|
+
"requiresReload:true needs `system reload`",
|
|
17761
|
+
"(or a manual browser reload) before Foundry acts on it."
|
|
15538
17762
|
].join("\n")
|
|
15539
17763
|
);
|
|
15540
17764
|
addNameFilterOption(addPaginationOptions(setting.command("list"))).action(
|
|
@@ -15555,6 +17779,46 @@ function registerSetting({ program, dependencies }) {
|
|
|
15555
17779
|
dependencies
|
|
15556
17780
|
});
|
|
15557
17781
|
});
|
|
17782
|
+
setting.command("get-many").description(
|
|
17783
|
+
"Read several settings in ONE call (an unknown id reports its own error, not the whole call)"
|
|
17784
|
+
).requiredOption(
|
|
17785
|
+
"--ids <list>",
|
|
17786
|
+
'Comma-separated setting ids in "namespace.key" form (e.g. core.chatBubbles,my-module.apiKey)',
|
|
17787
|
+
parseIdList
|
|
17788
|
+
).action(async function getManySettings(options) {
|
|
17789
|
+
await executeRemoteCommand({
|
|
17790
|
+
commandName: "setting.get-many",
|
|
17791
|
+
params: { ids: options.ids },
|
|
17792
|
+
command: this,
|
|
17793
|
+
dependencies
|
|
17794
|
+
});
|
|
17795
|
+
});
|
|
17796
|
+
setting.command("set").description("Write one registered setting").requiredOption("--namespace <namespace>", 'Setting namespace (e.g. "core" or a module id)').requiredOption("--key <key>", "Setting key within the namespace").requiredOption(
|
|
17797
|
+
"--value-json <json>",
|
|
17798
|
+
'New value as a JSON literal \u2014 true, 42, "text", [..] or {..}; a quoted string stays distinct from a boolean or a number',
|
|
17799
|
+
(value) => parseJsonValue(value, "--value-json")
|
|
17800
|
+
).action(async function setSetting(options) {
|
|
17801
|
+
await executeRemoteCommand({
|
|
17802
|
+
commandName: "setting.set",
|
|
17803
|
+
params: createSettingSetParams(options),
|
|
17804
|
+
command: this,
|
|
17805
|
+
dependencies
|
|
17806
|
+
});
|
|
17807
|
+
});
|
|
17808
|
+
setting.command("set-many").description("Write several settings in ONE call (stops at the first element Foundry refuses)").addOption(
|
|
17809
|
+
new Option12(
|
|
17810
|
+
"--items-json <json>",
|
|
17811
|
+
'Setting writes as a JSON ARRAY of {"namespace","key","value"} objects'
|
|
17812
|
+
).argParser((value) => parseSettingItems(value, "--items-json"))
|
|
17813
|
+
).addOption(new Option12("--items-stdin", "Read that JSON array from stdin instead").conflicts("itemsJson")).action(async function setManySettings(options) {
|
|
17814
|
+
const items = await resolveSettingItems(options, dependencies);
|
|
17815
|
+
await executeRemoteCommand({
|
|
17816
|
+
commandName: "setting.set-many",
|
|
17817
|
+
params: { items },
|
|
17818
|
+
command: this,
|
|
17819
|
+
dependencies
|
|
17820
|
+
});
|
|
17821
|
+
});
|
|
15558
17822
|
}
|
|
15559
17823
|
|
|
15560
17824
|
// src/commands/skill.ts
|
|
@@ -15762,10 +18026,20 @@ function registerSystem({ program, dependencies }) {
|
|
|
15762
18026
|
dependencies
|
|
15763
18027
|
});
|
|
15764
18028
|
});
|
|
18029
|
+
system.command("reload").description(
|
|
18030
|
+
"Reload the GM browser page. The bridge drops and reconnects on its own; pair it with a setting whose row reports requiresReload"
|
|
18031
|
+
).action(async function reload() {
|
|
18032
|
+
await executeRemoteCommand({
|
|
18033
|
+
commandName: "system.reload",
|
|
18034
|
+
params: {},
|
|
18035
|
+
command: this,
|
|
18036
|
+
dependencies
|
|
18037
|
+
});
|
|
18038
|
+
});
|
|
15765
18039
|
}
|
|
15766
18040
|
|
|
15767
18041
|
// src/commands/table.ts
|
|
15768
|
-
import { Option as
|
|
18042
|
+
import { Option as Option13 } from "commander";
|
|
15769
18043
|
function registerTable({ program, dependencies }) {
|
|
15770
18044
|
const { registerOwnershipSet, registerCompendiumImport } = createSharedRegistrars(dependencies);
|
|
15771
18045
|
const table = program.command("table").description("Foundry roll table commands");
|
|
@@ -15854,12 +18128,12 @@ Action verbs differ: \`table draw\` has NO .result.table \u2014 read .result.res
|
|
|
15854
18128
|
"--idempotency-key <key>",
|
|
15855
18129
|
"REQUIRED: client-supplied key so a retried draw returns the original result instead of drawing again. Reuse the SAME key across retries of one draw."
|
|
15856
18130
|
).addOption(
|
|
15857
|
-
new
|
|
18131
|
+
new Option13(
|
|
15858
18132
|
"--count <n>",
|
|
15859
18133
|
"Number of draw ITERATIONS, max 100 (>1 uses RollTable#drawMany); each iteration can return several rows, so results are not capped at this number"
|
|
15860
18134
|
).argParser(parsePositiveInt)
|
|
15861
18135
|
).addOption(
|
|
15862
|
-
new
|
|
18136
|
+
new Option13(
|
|
15863
18137
|
"--roll-mode <mode>",
|
|
15864
18138
|
"Chat visibility of the draw message (translated per Foundry version; default public)"
|
|
15865
18139
|
).choices([...TABLE_ROLL_MODES])
|
|
@@ -15929,7 +18203,7 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
|
|
|
15929
18203
|
});
|
|
15930
18204
|
addTableResultFieldOptions(
|
|
15931
18205
|
tableResult.command("update").requiredOption("--table-id <tableId>", "Roll table id").requiredOption("--result-id <resultId>", "Result (row) id").addOption(
|
|
15932
|
-
new
|
|
18206
|
+
new Option13("--range <low,high>", "New two-integer ascending roll range, e.g. 1,3").argParser(
|
|
15933
18207
|
parseResultRange
|
|
15934
18208
|
)
|
|
15935
18209
|
),
|
|
@@ -15944,7 +18218,7 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
|
|
|
15944
18218
|
});
|
|
15945
18219
|
addTableResultFieldOptions(
|
|
15946
18220
|
addIdempotencyKeyOption(tableResult.command("clone")).requiredOption("--table-id <tableId>", "Roll table id").requiredOption("--result-id <resultId>", "Source result (row) id").addOption(
|
|
15947
|
-
new
|
|
18221
|
+
new Option13("--range <low,high>", "Range override (two ascending integers), e.g. 4,6").argParser(
|
|
15948
18222
|
parseResultRange
|
|
15949
18223
|
)
|
|
15950
18224
|
),
|
|
@@ -15973,9 +18247,23 @@ Result key (--json): ${TABLE_RESULT_KEY_HELP}.`);
|
|
|
15973
18247
|
}
|
|
15974
18248
|
|
|
15975
18249
|
// src/commands/user.ts
|
|
18250
|
+
import { Option as Option14 } from "commander";
|
|
15976
18251
|
function registerUser({ program, dependencies }) {
|
|
15977
|
-
const user = program.command("user").description("Foundry user commands
|
|
15978
|
-
user.addHelpText(
|
|
18252
|
+
const user = program.command("user").description("Foundry user commands");
|
|
18253
|
+
user.addHelpText(
|
|
18254
|
+
"after",
|
|
18255
|
+
[
|
|
18256
|
+
"",
|
|
18257
|
+
"Result key (--json): .result.user (get, create, update) / .result.users[] (list). `delete`,",
|
|
18258
|
+
"`role set` and `permissions set` return their fields directly on .result.",
|
|
18259
|
+
"",
|
|
18260
|
+
"No user command accepts a password: Foundry sends passwords in the clear and hashes them",
|
|
18261
|
+
"server-side, so account credentials stay outside the bridge. Neither `role set` nor `delete`",
|
|
18262
|
+
"accepts the GM user the bridge itself runs through \u2014 the agent cannot demote or remove its",
|
|
18263
|
+
"own account.",
|
|
18264
|
+
"Roles are integers: 1=player, 2=trusted player, 3=assistant GM, 4=gamemaster."
|
|
18265
|
+
].join("\n")
|
|
18266
|
+
);
|
|
15979
18267
|
addNameFilterOption(addPaginationOptions(user.command("list"))).action(async function listUsers(options) {
|
|
15980
18268
|
await executeRemoteCommand({
|
|
15981
18269
|
commandName: "user.list",
|
|
@@ -15992,10 +18280,71 @@ function registerUser({ program, dependencies }) {
|
|
|
15992
18280
|
dependencies
|
|
15993
18281
|
});
|
|
15994
18282
|
});
|
|
18283
|
+
addUserFieldOptions(
|
|
18284
|
+
addIdempotencyKeyOption(user.command("create")).description("Create a player account (no password is set)").requiredOption("--name <name>", "User name").addOption(
|
|
18285
|
+
new Option14(
|
|
18286
|
+
"--role <role>",
|
|
18287
|
+
"Role for the new user (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster; default player)"
|
|
18288
|
+
).argParser(parseUserRole)
|
|
18289
|
+
),
|
|
18290
|
+
"create"
|
|
18291
|
+
).option("--data-json <json>", "Extra user fields (e.g. flags) as a JSON object (merged last)").action(async function createUser(options) {
|
|
18292
|
+
await executeRemoteCommand({
|
|
18293
|
+
commandName: "user.create",
|
|
18294
|
+
params: createUserCreateParams(options),
|
|
18295
|
+
command: this,
|
|
18296
|
+
dependencies
|
|
18297
|
+
});
|
|
18298
|
+
});
|
|
18299
|
+
addUserFieldOptions(
|
|
18300
|
+
user.command("update").description("Edit a user's presentation fields (the role has its own command)").requiredOption("--user-id <userId>", "User id").option("--name <name>", "New user name"),
|
|
18301
|
+
"update"
|
|
18302
|
+
).option("--patch-json <json>", "Extra user patch fields (e.g. flags) as a JSON object (merged last)").action(async function updateUser(options) {
|
|
18303
|
+
await executeRemoteCommand({
|
|
18304
|
+
commandName: "user.update",
|
|
18305
|
+
params: createUserUpdateParams(options),
|
|
18306
|
+
command: this,
|
|
18307
|
+
dependencies
|
|
18308
|
+
});
|
|
18309
|
+
});
|
|
18310
|
+
user.command("delete").description("Delete a user account (refuses the bridge's own GM user)").requiredOption("--user-id <userId>", "User id").action(async function deleteUser(options) {
|
|
18311
|
+
await executeRemoteCommand({
|
|
18312
|
+
commandName: "user.delete",
|
|
18313
|
+
params: { userId: options.userId },
|
|
18314
|
+
command: this,
|
|
18315
|
+
dependencies
|
|
18316
|
+
});
|
|
18317
|
+
});
|
|
18318
|
+
const userRole = user.command("role").description("User role (which capabilities the role grants)");
|
|
18319
|
+
userRole.command("set").description("Set a user's role (refuses the bridge's own GM user)").requiredOption("--user-id <userId>", "User id").requiredOption(
|
|
18320
|
+
"--role <role>",
|
|
18321
|
+
"New role (1=player, 2=trusted player, 3=assistant GM, 4=gamemaster)",
|
|
18322
|
+
parseUserRole
|
|
18323
|
+
).action(async function setUserRole(options) {
|
|
18324
|
+
await executeRemoteCommand({
|
|
18325
|
+
commandName: "user.role.set",
|
|
18326
|
+
params: { userId: options.userId, role: options.role },
|
|
18327
|
+
command: this,
|
|
18328
|
+
dependencies
|
|
18329
|
+
});
|
|
18330
|
+
});
|
|
18331
|
+
const userPermissions = user.command("permissions").description("Per-user permission overrides on top of the role defaults");
|
|
18332
|
+
userPermissions.command("set").description("Grant, revoke or drop per-user permission overrides").requiredOption("--user-id <userId>", "User id").requiredOption(
|
|
18333
|
+
"--permissions-json <json>",
|
|
18334
|
+
'Overrides as a JSON object, e.g. {"FILES_UPLOAD":true,"MACRO_SCRIPT":false,"BROADCAST_AUDIO":null} \u2014 true grants, false revokes, null drops the override so the role default applies again. Names are validated against the installed Foundry version.',
|
|
18335
|
+
parseUserPermissions
|
|
18336
|
+
).action(async function setUserPermissions(options) {
|
|
18337
|
+
await executeRemoteCommand({
|
|
18338
|
+
commandName: "user.permissions.set",
|
|
18339
|
+
params: { userId: options.userId, permissions: options.permissionsJson },
|
|
18340
|
+
command: this,
|
|
18341
|
+
dependencies
|
|
18342
|
+
});
|
|
18343
|
+
});
|
|
15995
18344
|
}
|
|
15996
18345
|
|
|
15997
18346
|
// src/commands/world.ts
|
|
15998
|
-
import { Option as
|
|
18347
|
+
import { Option as Option15 } from "commander";
|
|
15999
18348
|
function registerWorld({ program, dependencies }) {
|
|
16000
18349
|
const world = program.command("world").description("Foundry world-wide maintenance commands");
|
|
16001
18350
|
world.addHelpText(
|
|
@@ -16037,7 +18386,7 @@ function registerWorld({ program, dependencies }) {
|
|
|
16037
18386
|
});
|
|
16038
18387
|
addPaginationOptions(
|
|
16039
18388
|
world.command("search").description("Search world (and optionally compendium) documents through the bridge index").requiredOption("--query <query>", "Search query (2-256 characters; terms are AND-combined)").addOption(
|
|
16040
|
-
new
|
|
18389
|
+
new Option15(
|
|
16041
18390
|
"--mode <mode>",
|
|
16042
18391
|
'Fields to search: "name" (stored names only) or "full" (names + body text + the Actor/Item system walk, with snippets)'
|
|
16043
18392
|
).choices([...SEARCH_MODES]).default(SEARCH_MODES[0])
|
|
@@ -16046,7 +18395,7 @@ function registerWorld({ program, dependencies }) {
|
|
|
16046
18395
|
`Comma-separated document types to search: ${SEARCH_INDEXED_TYPES.join(",")} (default: all)`,
|
|
16047
18396
|
parseSearchTypeList
|
|
16048
18397
|
).option("--include-compendia", "Also search compendium pack entry NAMES (off by default)").addOption(
|
|
16049
|
-
new
|
|
18398
|
+
new Option15(
|
|
16050
18399
|
"--source <source>",
|
|
16051
18400
|
'Return only one section: "world", or "pack" (which requires --include-compendia)'
|
|
16052
18401
|
).choices([...SEARCH_SOURCES])
|
|
@@ -16077,7 +18426,7 @@ function createProgram(partialDependencies = {}) {
|
|
|
16077
18426
|
"Preview a mutation without persisting it (validates, resolves the target, runs all guards). Ignored for read commands.",
|
|
16078
18427
|
false
|
|
16079
18428
|
).addOption(
|
|
16080
|
-
new
|
|
18429
|
+
new Option16(
|
|
16081
18430
|
"--timeout-ms <ms>",
|
|
16082
18431
|
"Client wait for the daemon response, in ms (default 60000). Distinct from bridge serve --request-timeout-ms."
|
|
16083
18432
|
).argParser(parsePositiveInt)
|
|
@@ -16120,6 +18469,8 @@ function createProgram(partialDependencies = {}) {
|
|
|
16120
18469
|
registerCompendium({ program, dependencies });
|
|
16121
18470
|
registerFolder({ program, dependencies });
|
|
16122
18471
|
registerFile({ program, dependencies });
|
|
18472
|
+
registerGame({ program, dependencies });
|
|
18473
|
+
registerImage({ program, dependencies });
|
|
16123
18474
|
applyOutputRouting(program, dependencies);
|
|
16124
18475
|
applyExitOverride(program);
|
|
16125
18476
|
return program;
|