fvtt-world-cli 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ 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 = "1.1.2";
13
+ var PROTOCOL_VERSION = "1.2.0";
14
14
  var PROTOCOL_COMPONENTS = Object.freeze({
15
15
  MODULE: "module",
16
16
  CLI_DAEMON: "cli-daemon",
@@ -174,6 +174,7 @@ var ERROR_CODES = Object.freeze({
174
174
  TILE_NOT_FOUND: "TILE_NOT_FOUND",
175
175
  SOUND_NOT_FOUND: "SOUND_NOT_FOUND",
176
176
  WALL_NOT_FOUND: "WALL_NOT_FOUND",
177
+ LEVEL_NOT_FOUND: "LEVEL_NOT_FOUND",
177
178
  NOTE_NOT_FOUND: "NOTE_NOT_FOUND",
178
179
  DRAWING_NOT_FOUND: "DRAWING_NOT_FOUND",
179
180
  LIGHT_NOT_FOUND: "LIGHT_NOT_FOUND",
@@ -299,7 +300,7 @@ var includeProperty = {
299
300
  var actorIncludeProperty = {
300
301
  include: {
301
302
  type: "array",
302
- items: { type: "string", enum: ["flags", "effects", "items.flags", "items.effects", "prepared"] }
303
+ items: { type: "string", enum: ["flags", "effects", "items", "items.flags", "items.effects", "prepared"] }
303
304
  }
304
305
  };
305
306
  var batchIdsProperty = {
@@ -3153,6 +3154,68 @@ var executableBehaviorPatchSchema = {
3153
3154
  additionalProperties: true,
3154
3155
  minProperties: 1
3155
3156
  };
3157
+ var nullableNumberSchema = { type: ["number", "null"] };
3158
+ var levelImageProperties = {
3159
+ src: nullableStringSchema,
3160
+ tint: { type: "string", minLength: 1 },
3161
+ alphaThreshold: { type: "number" }
3162
+ };
3163
+ var levelCreateSchema = {
3164
+ type: "object",
3165
+ required: ["name"],
3166
+ properties: {
3167
+ name: { type: "string", minLength: 1 },
3168
+ elevation: {
3169
+ type: "object",
3170
+ required: [],
3171
+ properties: { bottom: nullableNumberSchema, top: nullableNumberSchema },
3172
+ additionalProperties: false
3173
+ },
3174
+ background: {
3175
+ type: "object",
3176
+ required: [],
3177
+ properties: { color: { type: "string", minLength: 1 }, ...levelImageProperties },
3178
+ additionalProperties: false
3179
+ },
3180
+ foreground: {
3181
+ type: "object",
3182
+ required: [],
3183
+ properties: levelImageProperties,
3184
+ additionalProperties: false
3185
+ },
3186
+ fog: {
3187
+ type: "object",
3188
+ required: [],
3189
+ properties: { src: nullableStringSchema },
3190
+ additionalProperties: false
3191
+ },
3192
+ textures: {
3193
+ type: "object",
3194
+ required: [],
3195
+ properties: {
3196
+ anchorX: { type: "number" },
3197
+ anchorY: { type: "number" },
3198
+ offsetX: { type: "integer" },
3199
+ offsetY: { type: "integer" },
3200
+ fit: { type: "string", minLength: 1 },
3201
+ scaleX: { type: "number" },
3202
+ scaleY: { type: "number" },
3203
+ rotation: { type: "number" }
3204
+ },
3205
+ additionalProperties: false
3206
+ },
3207
+ visibility: {
3208
+ type: "object",
3209
+ required: [],
3210
+ properties: { levels: { type: "array", items: { type: "string", minLength: 1 } } },
3211
+ additionalProperties: false
3212
+ },
3213
+ sort: { type: "integer" },
3214
+ flags: freeformObjectSchema
3215
+ },
3216
+ additionalProperties: false
3217
+ };
3218
+ var levelPatchSchema = patchFrom(levelCreateSchema);
3156
3219
  var sceneEmbeddedCommands = {
3157
3220
  "scene.token.list": cmd({
3158
3221
  type: "object",
@@ -3744,6 +3807,85 @@ var sceneEmbeddedCommands = {
3744
3807
  createSchema: wallCreateSchema,
3745
3808
  patchSchema: wallPatchSchema
3746
3809
  }),
3810
+ "scene.level.list": cmd({
3811
+ type: "object",
3812
+ required: ["sceneId"],
3813
+ properties: {
3814
+ sceneId: { type: "string", minLength: 1 },
3815
+ ...paginationProperties
3816
+ },
3817
+ additionalProperties: false
3818
+ }),
3819
+ "scene.level.get": cmd({
3820
+ type: "object",
3821
+ required: ["sceneId", "levelId"],
3822
+ properties: {
3823
+ sceneId: { type: "string", minLength: 1 },
3824
+ levelId: { type: "string", minLength: 1 }
3825
+ },
3826
+ additionalProperties: false
3827
+ }),
3828
+ "scene.level.create": cmd(
3829
+ {
3830
+ type: "object",
3831
+ required: ["sceneId", "data"],
3832
+ properties: {
3833
+ sceneId: { type: "string", minLength: 1 },
3834
+ data: levelCreateSchema,
3835
+ ...dryRunProperty,
3836
+ ...idempotencyKeyProperty
3837
+ },
3838
+ additionalProperties: false
3839
+ },
3840
+ { mutation: true }
3841
+ ),
3842
+ "scene.level.update": cmd(
3843
+ {
3844
+ type: "object",
3845
+ required: ["sceneId", "levelId", "patch"],
3846
+ properties: {
3847
+ sceneId: { type: "string", minLength: 1 },
3848
+ levelId: { type: "string", minLength: 1 },
3849
+ patch: levelPatchSchema,
3850
+ ...dryRunProperty
3851
+ },
3852
+ additionalProperties: false
3853
+ },
3854
+ { mutation: true }
3855
+ ),
3856
+ "scene.level.delete": cmd(
3857
+ {
3858
+ type: "object",
3859
+ required: ["sceneId", "levelId"],
3860
+ properties: {
3861
+ sceneId: { type: "string", minLength: 1 },
3862
+ levelId: { type: "string", minLength: 1 },
3863
+ ...dryRunProperty
3864
+ },
3865
+ additionalProperties: false
3866
+ },
3867
+ { mutation: true }
3868
+ ),
3869
+ "scene.level.clone": cmd(
3870
+ {
3871
+ type: "object",
3872
+ required: ["sceneId", "levelId"],
3873
+ properties: {
3874
+ sceneId: { type: "string", minLength: 1 },
3875
+ levelId: { type: "string", minLength: 1 },
3876
+ patch: levelPatchSchema,
3877
+ ...dryRunProperty,
3878
+ ...idempotencyKeyProperty
3879
+ },
3880
+ additionalProperties: false
3881
+ },
3882
+ { mutation: true }
3883
+ ),
3884
+ ...batchWriteCommands("scene.level", {
3885
+ scopeProperties: sceneScopeProperties,
3886
+ createSchema: levelCreateSchema,
3887
+ patchSchema: levelPatchSchema
3888
+ }),
3747
3889
  "scene.note.list": cmd({
3748
3890
  type: "object",
3749
3891
  required: ["sceneId"],
@@ -4288,6 +4430,10 @@ var sceneBackgroundSchema = {
4288
4430
  scaleX: { type: "number" },
4289
4431
  scaleY: { type: "number" },
4290
4432
  rotation: { type: "number" },
4433
+ anchorX: { type: "number" },
4434
+ anchorY: { type: "number" },
4435
+ fit: { type: "string", minLength: 1 },
4436
+ alphaThreshold: { type: "number" },
4291
4437
  tint: { type: "string", minLength: 1 }
4292
4438
  },
4293
4439
  additionalProperties: false,
@@ -4299,15 +4445,12 @@ var sceneGridSchema = {
4299
4445
  properties: {
4300
4446
  type: { type: "number" },
4301
4447
  size: { type: "number" },
4302
- sizeX: { type: "number" },
4303
- sizeY: { type: "number" },
4304
4448
  distance: { type: "number" },
4305
4449
  units: { type: "string" },
4306
4450
  style: { type: "string" },
4307
4451
  thickness: { type: "number" },
4308
4452
  color: { type: "string", minLength: 1 },
4309
- alpha: { type: "number" },
4310
- diagonals: { type: "number" }
4453
+ alpha: { type: "number" }
4311
4454
  },
4312
4455
  additionalProperties: false,
4313
4456
  minProperties: 1
@@ -4317,6 +4460,7 @@ var sceneCreateSchema = {
4317
4460
  required: ["name"],
4318
4461
  properties: {
4319
4462
  name: { type: "string", minLength: 1 },
4463
+ folder: nullableStringSchema,
4320
4464
  active: { type: "boolean" },
4321
4465
  navigation: { type: "boolean" },
4322
4466
  navOrder: { type: "number" },
@@ -4333,7 +4477,7 @@ var sceneCreateSchema = {
4333
4477
  navName: nullableStringSchema,
4334
4478
  thumb: nullableStringSchema,
4335
4479
  sort: { type: "number" },
4336
- initialLevel: nullableStringSchema,
4480
+ levels: { type: "array", minItems: 1, items: levelCreateSchema },
4337
4481
  playlist: nullableStringSchema,
4338
4482
  playlistSound: nullableStringSchema,
4339
4483
  journal: nullableStringSchema,
@@ -4348,7 +4492,10 @@ var sceneCreateSchema = {
4348
4492
  },
4349
4493
  additionalProperties: false
4350
4494
  };
4351
- var scenePatchSchema = patchFrom(sceneCreateSchema);
4495
+ var scenePatchSchema = patchFrom(sceneCreateSchema, {
4496
+ omit: ["levels"],
4497
+ prepend: { initialLevel: nullableStringSchema }
4498
+ });
4352
4499
  var sceneCommands = {
4353
4500
  "scene.list": cmd(nameFilteredListSchema),
4354
4501
  "scene.get": cmd(sceneIdSchema),
@@ -4561,7 +4708,14 @@ var settingCommands = {
4561
4708
  // ../protocol/src/schemas/system.js
4562
4709
  var systemCommands = {
4563
4710
  "system.ping": cmd(emptyObjectSchema),
4564
- "system.info": cmd(emptyObjectSchema),
4711
+ "system.info": cmd({
4712
+ type: "object",
4713
+ required: [],
4714
+ properties: {
4715
+ include: { type: "array", items: { type: "string", enum: ["modules"] } }
4716
+ },
4717
+ additionalProperties: false
4718
+ }),
4565
4719
  "system.reload": cmd(emptyObjectSchema)
4566
4720
  };
4567
4721
 
@@ -5257,6 +5411,15 @@ var DEFAULT_COMMAND_PROFILE = deepFreeze({
5257
5411
  "scene.wall.create-many": "allow",
5258
5412
  "scene.wall.update-many": "allow",
5259
5413
  "scene.wall.delete-many": "approve",
5414
+ "scene.level.list": "allow",
5415
+ "scene.level.get": "allow",
5416
+ "scene.level.create": "allow",
5417
+ "scene.level.update": "allow",
5418
+ "scene.level.delete": "approve",
5419
+ "scene.level.clone": "allow",
5420
+ "scene.level.create-many": "allow",
5421
+ "scene.level.update-many": "allow",
5422
+ "scene.level.delete-many": "approve",
5260
5423
  "scene.note.list": "allow",
5261
5424
  "scene.note.get": "allow",
5262
5425
  "scene.note.create": "allow",
@@ -6119,11 +6282,22 @@ function createCliConfigStore(configPath = getDefaultCliConfigPath()) {
6119
6282
  }
6120
6283
  };
6121
6284
  }
6122
- function mergePersistedCliConfig(existingConfig, updates) {
6123
- return PersistedCliConfigSchema.parse({
6124
- ...existingConfig ?? createEmptyConfig(),
6125
- ...updates
6126
- });
6285
+ var DAEMON_OWNED_CONFIG_KEYS = ["version", "daemonUrl", "deviceCredential", "pairings"];
6286
+ function withDaemonOwnedKeys(config, owner) {
6287
+ const merged = { ...config };
6288
+ for (const key of DAEMON_OWNED_CONFIG_KEYS) {
6289
+ if (owner[key] === void 0) delete merged[key];
6290
+ else merged[key] = owner[key];
6291
+ }
6292
+ return merged;
6293
+ }
6294
+ function writeCliConfigUpdate(configStore, updates) {
6295
+ configStore.writeConfig(
6296
+ PersistedCliConfigSchema.parse({
6297
+ ...configStore.readConfig() ?? createEmptyConfig(),
6298
+ ...updates
6299
+ })
6300
+ );
6127
6301
  }
6128
6302
  function resolveRemoteCommandConfig({
6129
6303
  flagDaemonUrl,
@@ -6169,8 +6343,8 @@ function readPendingApprovalDetails(envelope) {
6169
6343
  var DaemonTransportError = class extends Error {
6170
6344
  code;
6171
6345
  details;
6172
- constructor(code, message, details) {
6173
- super(message);
6346
+ constructor(code, message, details, options) {
6347
+ super(message, options);
6174
6348
  this.name = "DaemonTransportError";
6175
6349
  this.code = code;
6176
6350
  this.details = details;
@@ -6615,7 +6789,7 @@ var BridgeSessionStore = class {
6615
6789
  ok: false,
6616
6790
  error: createProtocolError({
6617
6791
  code: ERROR_CODES.UNKNOWN_COMMAND,
6618
- message: `Bridge session does not advertise support for ${request.command}; run system.info to see the connected bridge's supported command inventory (the module may be older than this CLI)`,
6792
+ message: `Bridge session does not advertise support for ${request.command}; details.supportedCommands lists the commands the connected bridge advertised, and \`fvtt-world-cli auth status\` reports them without the internal approval and policy commands; the Foundry module may be older than this CLI, or the GM page may need a reload`,
6619
6793
  details: {
6620
6794
  command: request.command,
6621
6795
  supportedCommands: this.activeSession.commands
@@ -7161,11 +7335,9 @@ var BridgeDaemon = class {
7161
7335
  }
7162
7336
  persistConfig() {
7163
7337
  if (!this.configStore) return;
7164
- const persisted = this.configStore.readConfig();
7165
- this.config = PersistedCliConfigSchema.parse({
7166
- ...this.config,
7167
- ...persisted?.uploadLimitBytes === void 0 ? {} : { uploadLimitBytes: persisted.uploadLimitBytes }
7168
- });
7338
+ this.config = PersistedCliConfigSchema.parse(
7339
+ withDaemonOwnedKeys(this.configStore.readConfig() ?? this.config, this.config)
7340
+ );
7169
7341
  this.configStore.writeConfig(this.config);
7170
7342
  }
7171
7343
  handlePairingRequest(socket, state, message) {
@@ -7972,23 +8144,35 @@ async function connectDaemonClient({
7972
8144
  socket.send(JSON.stringify(createClientHello({ credential: deviceCredential ?? "" })));
7973
8145
  });
7974
8146
  socket.once("message", (data) => {
7975
- const parsed = parseBridgeMessage(normalizeIncomingData(data));
7976
- const skew = parsed.ok ? protocolVersionSkewError(parsed.value) : null;
7977
- if (skew) {
8147
+ const decoded = decodeTransportFrame(data);
8148
+ if (!decoded.ok) {
8149
+ clearTimeout(timer);
8150
+ reject(decoded.error);
8151
+ socket.close();
8152
+ return;
8153
+ }
8154
+ const message = decoded.value;
8155
+ if (message.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK) {
7978
8156
  clearTimeout(timer);
7979
- reject(skew);
8157
+ reject(
8158
+ new DaemonTransportError(
8159
+ ERROR_CODES.INVALID_MESSAGE,
8160
+ "Daemon returned an unexpected message type",
8161
+ {
8162
+ reason: "unexpected_type"
8163
+ }
8164
+ )
8165
+ );
7980
8166
  socket.close();
7981
8167
  return;
7982
8168
  }
7983
- const validation = parsed.ok ? validateTransportMessage(parsed.value) : { ok: false };
7984
- const message = parsed.ok && validation.ok ? parsed.value : null;
7985
- if (!message || message.type !== MESSAGE_TYPES.CLIENT_HELLO_ACK || !message.ok) {
8169
+ if (!message.ok) {
7986
8170
  clearTimeout(timer);
7987
8171
  reject(
7988
8172
  new DaemonTransportError(
7989
- message?.error?.code ?? ERROR_CODES.UNAUTHORIZED,
7990
- message?.error?.message ?? "Daemon rejected client authentication",
7991
- message?.error?.details
8173
+ message.error?.code ?? ERROR_CODES.UNAUTHORIZED,
8174
+ message.error?.message ?? "Daemon rejected client authentication",
8175
+ message.error?.details
7992
8176
  )
7993
8177
  );
7994
8178
  socket.close();
@@ -8003,7 +8187,8 @@ async function connectDaemonClient({
8003
8187
  new DaemonTransportError(
8004
8188
  ERROR_CODES.DAEMON_UNAVAILABLE,
8005
8189
  error instanceof Error ? error.message : String(error),
8006
- { reason: "connect_error" }
8190
+ { reason: "connect_error" },
8191
+ { cause: error }
8007
8192
  )
8008
8193
  );
8009
8194
  });
@@ -8523,23 +8708,21 @@ function parseCsvList(value, label) {
8523
8708
  }
8524
8709
  return items;
8525
8710
  }
8526
- var INCLUDE_ALLOWED = ["flags", "effects"];
8527
- function parseIncludeFields(value) {
8711
+ function parseAllowedIncludeFields(value, allowed) {
8528
8712
  const fields = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
8529
8713
  if (fields.length === 0) {
8530
- throw new InvalidArgumentError(
8531
- `--include must name at least one field (allowed: ${INCLUDE_ALLOWED.join(", ")})`
8532
- );
8714
+ throw new InvalidArgumentError(`--include must name at least one field (allowed: ${allowed.join(", ")})`);
8533
8715
  }
8534
8716
  for (const field of fields) {
8535
- if (!INCLUDE_ALLOWED.includes(field)) {
8536
- throw new InvalidArgumentError(
8537
- `--include only supports ${INCLUDE_ALLOWED.join(", ")}, received ${field}`
8538
- );
8717
+ if (!allowed.includes(field)) {
8718
+ throw new InvalidArgumentError(`--include only supports ${allowed.join(", ")}, received ${field}`);
8539
8719
  }
8540
8720
  }
8541
8721
  return fields;
8542
8722
  }
8723
+ function parseIncludeFields(value) {
8724
+ return parseAllowedIncludeFields(value, ["flags", "effects"]);
8725
+ }
8543
8726
  var OWNERSHIP_LEVELS = [-1, 0, 1, 2, 3];
8544
8727
  var INTEGER_PATTERN = /^-?\d+$/;
8545
8728
  function parseOwnershipLevel(value) {
@@ -8639,56 +8822,24 @@ function parseWhisperIds(value) {
8639
8822
  }
8640
8823
  return ids;
8641
8824
  }
8642
- var ACTOR_INCLUDE_ALLOWED = ["flags", "effects", "items.flags", "items.effects", "prepared"];
8643
8825
  function parseActorIncludeFields(value) {
8644
- const fields = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
8645
- if (fields.length === 0) {
8646
- throw new InvalidArgumentError(
8647
- `--include must name at least one field (allowed: ${ACTOR_INCLUDE_ALLOWED.join(", ")})`
8648
- );
8649
- }
8650
- for (const field of fields) {
8651
- if (!ACTOR_INCLUDE_ALLOWED.includes(field)) {
8652
- throw new InvalidArgumentError(
8653
- `--include only supports ${ACTOR_INCLUDE_ALLOWED.join(", ")}, received ${field}`
8654
- );
8655
- }
8656
- }
8657
- return fields;
8826
+ return parseAllowedIncludeFields(value, [
8827
+ "flags",
8828
+ "effects",
8829
+ "items",
8830
+ "items.flags",
8831
+ "items.effects",
8832
+ "prepared"
8833
+ ]);
8658
8834
  }
8659
- var TOKEN_INCLUDE_ALLOWED = ["prepared"];
8660
8835
  function parseTokenIncludeFields(value) {
8661
- const fields = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
8662
- if (fields.length === 0) {
8663
- throw new InvalidArgumentError(
8664
- `--include must name at least one field (allowed: ${TOKEN_INCLUDE_ALLOWED.join(", ")})`
8665
- );
8666
- }
8667
- for (const field of fields) {
8668
- if (!TOKEN_INCLUDE_ALLOWED.includes(field)) {
8669
- throw new InvalidArgumentError(
8670
- `--include only supports ${TOKEN_INCLUDE_ALLOWED.join(", ")}, received ${field}`
8671
- );
8672
- }
8673
- }
8674
- return fields;
8836
+ return parseAllowedIncludeFields(value, ["prepared"]);
8837
+ }
8838
+ function parseSystemInfoIncludeFields(value) {
8839
+ return parseAllowedIncludeFields(value, ["modules"]);
8675
8840
  }
8676
- var COMPENDIUM_INCLUDE_ALLOWED = ["effects"];
8677
8841
  function parseCompendiumIncludeFields(value) {
8678
- const fields = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
8679
- if (fields.length === 0) {
8680
- throw new InvalidArgumentError(
8681
- `--include must name at least one field (allowed: ${COMPENDIUM_INCLUDE_ALLOWED.join(", ")})`
8682
- );
8683
- }
8684
- for (const field of fields) {
8685
- if (!COMPENDIUM_INCLUDE_ALLOWED.includes(field)) {
8686
- throw new InvalidArgumentError(
8687
- `--include only supports ${COMPENDIUM_INCLUDE_ALLOWED.join(", ")}, received ${field}`
8688
- );
8689
- }
8690
- }
8691
- return fields;
8842
+ return parseAllowedIncludeFields(value, ["effects"]);
8692
8843
  }
8693
8844
  function parseCompendiumFieldsList(value) {
8694
8845
  return value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
@@ -8786,44 +8937,10 @@ function createItemUpdateParams(options) {
8786
8937
  ...options.include ? { include: options.include } : {}
8787
8938
  };
8788
8939
  }
8789
- function createSceneUpdateParams(options) {
8790
- return {
8791
- sceneId: options.sceneId,
8792
- patch: assertNonEmptyPatch({
8793
- ...stringField("name", options.name),
8794
- ...booleanField("active", options.active),
8795
- ...booleanField("navigation", options.navigation),
8796
- ...numberField("navOrder", options.navOrder),
8797
- ...numberField("width", options.width),
8798
- ...numberField("height", options.height),
8799
- ...jsonObjectField("grid", options.gridJson, "--grid-json"),
8800
- ...jsonObjectField("background", options.backgroundJson, "--background-json"),
8801
- ...booleanField("tokenVision", options.tokenVision),
8802
- ...truthyStringField("weather", options.weather),
8803
- ...numberField("padding", options.padding),
8804
- ...numberField("shiftX", options.shiftX),
8805
- ...numberField("shiftY", options.shiftY),
8806
- ...truthyStringField("navName", options.navName),
8807
- ...truthyStringField("thumb", options.thumb),
8808
- ...truthyStringField("foreground", options.foreground),
8809
- ...numberField("foregroundElevation", options.foregroundElevation),
8810
- ...numberField("sort", options.sort),
8811
- ...truthyStringField("initialLevel", options.initialLevel),
8812
- ...truthyStringField("playlist", options.playlist),
8813
- ...truthyStringField("playlistSound", options.playlistSound),
8814
- ...truthyStringField("journal", options.journal),
8815
- ...truthyStringField("journalEntryPage", options.journalEntryPage),
8816
- ...jsonObjectField("environment", options.environmentJson, "--environment-json"),
8817
- ...jsonObjectField("fog", options.fogJson, "--fog-json"),
8818
- ...jsonObjectField("initial", options.initialJson, "--initial-json"),
8819
- ...jsonObjectField("transition", options.transitionJson, "--transition-json"),
8820
- ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
8821
- })
8822
- };
8823
- }
8824
8940
  function buildScenePatch(options) {
8825
8941
  return {
8826
8942
  ...stringField("name", options.name),
8943
+ ...folderField(options),
8827
8944
  ...booleanField("active", options.active),
8828
8945
  ...booleanField("navigation", options.navigation),
8829
8946
  ...numberField("navOrder", options.navOrder),
@@ -8852,6 +8969,15 @@ function buildScenePatch(options) {
8852
8969
  ...jsonObjectField("transition", options.transitionJson, "--transition-json")
8853
8970
  };
8854
8971
  }
8972
+ function createSceneUpdateParams(options) {
8973
+ return {
8974
+ sceneId: options.sceneId,
8975
+ patch: assertNonEmptyPatch({
8976
+ ...buildScenePatch(options),
8977
+ ...optionalJsonObject(options.patchJson, "--patch-json") ?? {}
8978
+ })
8979
+ };
8980
+ }
8855
8981
  function createSceneCreateParams(options) {
8856
8982
  return {
8857
8983
  data: {
@@ -9657,6 +9783,23 @@ function createSceneWallCloneParams(options) {
9657
9783
  ...optionalPatch(optionalJsonObject(options.patchJson, "--patch-json") ?? {})
9658
9784
  };
9659
9785
  }
9786
+ function createSceneLevelCreateParams(options) {
9787
+ return { sceneId: options.sceneId, data: optionalJsonObject(options.dataJson, "--data-json") ?? {} };
9788
+ }
9789
+ function createSceneLevelUpdateParams(options) {
9790
+ return {
9791
+ sceneId: options.sceneId,
9792
+ levelId: options.levelId,
9793
+ patch: assertNonEmptyPatch(optionalJsonObject(options.patchJson, "--patch-json") ?? {})
9794
+ };
9795
+ }
9796
+ function createSceneLevelCloneParams(options) {
9797
+ return {
9798
+ sceneId: options.sceneId,
9799
+ levelId: options.levelId,
9800
+ ...optionalPatch(optionalJsonObject(options.patchJson, "--patch-json") ?? {})
9801
+ };
9802
+ }
9660
9803
  function createSceneNoteCreateParams(options) {
9661
9804
  return { sceneId: options.sceneId, data: optionalJsonObject(options.dataJson, "--data-json") ?? {} };
9662
9805
  }
@@ -10369,7 +10512,7 @@ function renderActorDetails(actor) {
10369
10512
  if (actor && Object.hasOwn(actor, "prepared")) {
10370
10513
  lines.push(`prepared: ${Object.keys(actor.prepared ?? {}).join(", ")} (use --json for values)`);
10371
10514
  }
10372
- lines.push(`items: ${items.length}`);
10515
+ lines.push(`items: ${actor?.itemCount ?? items.length}`);
10373
10516
  items.forEach((item) => {
10374
10517
  lines.push(`item: ${renderItemSummaryLine(item)}`);
10375
10518
  });
@@ -11128,16 +11271,23 @@ function renderSceneDetails(scene) {
11128
11271
  const lines = [
11129
11272
  `id: ${scene?.id}`,
11130
11273
  `name: ${scene?.name}`,
11274
+ `folder: ${scene?.folder ?? ""}`,
11131
11275
  `active: ${String(Boolean(scene?.active))}`,
11132
11276
  `navigation: ${String(Boolean(scene?.navigation))}`,
11133
11277
  `nav order: ${scene?.navOrder ?? 0}`,
11134
11278
  `size: ${scene?.width}x${scene?.height}`,
11135
11279
  `grid: ${JSON.stringify(scene?.grid ?? null)}`,
11136
- `background: ${JSON.stringify(scene?.background ?? null)}`,
11280
+ ...scene && Object.hasOwn(scene, "background") ? [`background: ${JSON.stringify(scene.background ?? null)}`] : [],
11137
11281
  `token vision: ${String(Boolean(scene?.tokenVision))}`,
11138
11282
  `weather: ${scene?.weather ?? ""}`,
11139
11283
  `playlist: ${scene?.playlist ?? ""}`
11140
11284
  ];
11285
+ if (scene && Object.hasOwn(scene, "initialLevel")) {
11286
+ lines.push(`initial level: ${scene.initialLevel ?? "(first level)"}`);
11287
+ }
11288
+ for (const level of Array.isArray(scene?.levels) ? scene.levels : []) {
11289
+ lines.push(`level: ${renderLevelRow(level)}`);
11290
+ }
11141
11291
  lines.push(...renderCompendiumSourceLines(scene));
11142
11292
  if (scene && Object.hasOwn(scene, "ownership")) {
11143
11293
  lines.push(...renderOwnershipLines(scene.ownership));
@@ -11149,6 +11299,20 @@ function renderSceneDetails(scene) {
11149
11299
  }
11150
11300
  return lines.join("\n");
11151
11301
  }
11302
+ function levelMembership(placeable) {
11303
+ if (placeable && Object.hasOwn(placeable, "level")) return ["level", String(placeable.level)];
11304
+ if (placeable && Object.hasOwn(placeable, "levels"))
11305
+ return ["levels", placeable.levels.join(",") || "(all)"];
11306
+ return null;
11307
+ }
11308
+ function levelMembershipLines(placeable) {
11309
+ const membership = levelMembership(placeable);
11310
+ return membership ? [`${membership[0]}: ${membership[1]}`] : [];
11311
+ }
11312
+ function levelMembershipCell(placeable) {
11313
+ const membership = levelMembership(placeable);
11314
+ return membership ? ` ${membership[0]}=${membership[1]}` : "";
11315
+ }
11152
11316
  function renderTokenDetails(sceneId, token) {
11153
11317
  const lines = [
11154
11318
  `scene: ${sceneId}`,
@@ -11158,6 +11322,7 @@ function renderTokenDetails(sceneId, token) {
11158
11322
  `actorLink: ${String(Boolean(token?.actorLink))}`,
11159
11323
  `position: (${token?.x ?? 0}, ${token?.y ?? 0})`,
11160
11324
  `elevation: ${token?.elevation ?? 0}`,
11325
+ ...levelMembershipLines(token),
11161
11326
  `rotation: ${token?.rotation ?? 0}`,
11162
11327
  `hidden: ${String(Boolean(token?.hidden))}`,
11163
11328
  `disposition: ${token?.disposition ?? ""}`,
@@ -11206,6 +11371,7 @@ function renderTileDetails(sceneId, tile) {
11206
11371
  `size: ${tile?.width}x${tile?.height}`,
11207
11372
  `rotation: ${tile?.rotation ?? 0}`,
11208
11373
  `elevation: ${tile?.elevation ?? 0}`,
11374
+ ...levelMembershipLines(tile),
11209
11375
  `sort: ${tile?.sort ?? 0}`,
11210
11376
  `hidden: ${String(Boolean(tile?.hidden))}`,
11211
11377
  `locked: ${String(Boolean(tile?.locked))}`,
@@ -11219,6 +11385,7 @@ function renderSoundDetails(sceneId, sound) {
11219
11385
  `path: ${sound?.path ?? ""}`,
11220
11386
  `position: (${sound?.x ?? 0}, ${sound?.y ?? 0})`,
11221
11387
  `radius: ${sound?.radius ?? 0}`,
11388
+ ...levelMembershipLines(sound),
11222
11389
  `volume: ${sound?.volume ?? ""}`,
11223
11390
  `walls: ${String(Boolean(sound?.walls))}`,
11224
11391
  `easing: ${String(Boolean(sound?.easing))}`,
@@ -11226,11 +11393,30 @@ function renderSoundDetails(sceneId, sound) {
11226
11393
  `hidden: ${String(Boolean(sound?.hidden))}`
11227
11394
  ].join("\n");
11228
11395
  }
11396
+ function renderLevelRow(level) {
11397
+ const elevation = level?.elevation ?? {};
11398
+ return `${level?.id} ${level?.name ?? ""} elevation=${elevation.bottom ?? "-inf"}..${elevation.top ?? "+inf"} sort=${level?.sort ?? 0}`;
11399
+ }
11400
+ function renderLevelDetails(sceneId, level) {
11401
+ return [
11402
+ `scene: ${sceneId}`,
11403
+ `id: ${level?.id}`,
11404
+ `name: ${level?.name ?? ""}`,
11405
+ `elevation: ${JSON.stringify(level?.elevation ?? null)}`,
11406
+ `sort: ${level?.sort ?? 0}`,
11407
+ `background: ${JSON.stringify(level?.background ?? null)}`,
11408
+ `foreground: ${JSON.stringify(level?.foreground ?? null)}`,
11409
+ `fog: ${JSON.stringify(level?.fog ?? null)}`,
11410
+ `textures: ${JSON.stringify(level?.textures ?? null)}`,
11411
+ `visibility: ${JSON.stringify(level?.visibility ?? null)}`
11412
+ ].join("\n");
11413
+ }
11229
11414
  function renderWallDetails(sceneId, wall) {
11230
11415
  return [
11231
11416
  `scene: ${sceneId}`,
11232
11417
  `id: ${wall?.id}`,
11233
11418
  `c: ${JSON.stringify(wall?.c ?? null)}`,
11419
+ ...levelMembershipLines(wall),
11234
11420
  `door: ${wall?.door ?? 0}`,
11235
11421
  `ds: ${wall?.ds ?? 0}`,
11236
11422
  `doorSound: ${wall?.doorSound ?? ""}`,
@@ -11253,6 +11439,7 @@ function renderNoteDetails(sceneId, note) {
11253
11439
  `texture: ${JSON.stringify(note?.texture ?? null)}`,
11254
11440
  `iconSize: ${note?.iconSize ?? ""}`,
11255
11441
  `elevation: ${note?.elevation ?? 0}`,
11442
+ ...levelMembershipLines(note),
11256
11443
  `sort: ${note?.sort ?? 0}`,
11257
11444
  `global: ${String(Boolean(note?.global))}`
11258
11445
  ].join("\n");
@@ -11268,6 +11455,7 @@ function renderDrawingDetails(sceneId, drawing) {
11268
11455
  `shape: ${JSON.stringify(drawing?.shape ?? null)}`,
11269
11456
  `rotation: ${drawing?.rotation ?? 0}`,
11270
11457
  `elevation: ${drawing?.elevation ?? 0}`,
11458
+ ...levelMembershipLines(drawing),
11271
11459
  `sort: ${drawing?.sort ?? 0}`,
11272
11460
  `fillColor: ${drawing?.fillColor ?? ""}`,
11273
11461
  `strokeColor: ${drawing?.strokeColor ?? ""}`,
@@ -11282,6 +11470,7 @@ function renderLightDetails(sceneId, light) {
11282
11470
  `name: ${light?.name ?? ""}`,
11283
11471
  `position: (${light?.x ?? 0}, ${light?.y ?? 0})`,
11284
11472
  `elevation: ${light?.elevation ?? 0}`,
11473
+ ...levelMembershipLines(light),
11285
11474
  `rotation: ${light?.rotation ?? 0}`,
11286
11475
  `walls: ${String(Boolean(light?.walls))}`,
11287
11476
  `vision: ${String(Boolean(light?.vision))}`,
@@ -11312,6 +11501,7 @@ function renderRegionDetails(sceneId, region) {
11312
11501
  `color: ${region?.color ?? ""}`,
11313
11502
  `visibility: ${region?.visibility ?? ""}`,
11314
11503
  `elevation: ${JSON.stringify(region?.elevation ?? null)}`,
11504
+ ...levelMembershipLines(region),
11315
11505
  `shapes: ${JSON.stringify(region?.shapes ?? [])}`,
11316
11506
  `behaviors: ${JSON.stringify(region?.behaviors ?? [])}`,
11317
11507
  `locked: ${String(Boolean(region?.locked))}`
@@ -11614,10 +11804,12 @@ var RENDERERS = registerRenderers([
11614
11804
  `world: ${result.world?.title ?? "Unknown World"} [${result.world?.id ?? "unknown-world"}]`,
11615
11805
  `user: ${result.user?.name ?? "Unknown User"} [${result.user?.id ?? "unknown-user"}] GM=${String(Boolean(result.user?.isGM))}`,
11616
11806
  `bridge: ${result.bridge?.status ?? "unknown"}`,
11617
- `modules: ${(result.modules ?? []).length}`,
11807
+ `modules: ${result.moduleCount ?? "unknown"} installed, ${result.activeModuleCount ?? "unknown"} active`,
11808
+ ...(result.modules ?? []).map(
11809
+ (module) => ` ${module.id} ${module.version ?? "unknown"} ${module.active ? "active" : "inactive"}`
11810
+ ),
11618
11811
  `limits.uploadBytes: ${result.limits?.uploadBytes ?? "unknown"} (source: ${result.limits?.uploadSource ?? "unknown"})`,
11619
- `limits.wsMaxPayloadBytes: ${result.limits?.wsMaxPayloadBytes ?? "unknown"}`,
11620
- `commands: ${(result.commands ?? []).join(", ")}`
11812
+ `limits.wsMaxPayloadBytes: ${result.limits?.wsMaxPayloadBytes ?? "unknown"}`
11621
11813
  ].join("\n")
11622
11814
  ),
11623
11815
  detailRenderer(
@@ -12348,7 +12540,7 @@ var RENDERERS = registerRenderers([
12348
12540
  empty: "No tokens found.",
12349
12541
  heading: "Tokens",
12350
12542
  prefix: scenePrefix,
12351
- row: (token) => `${token.id} ${token.name} actor=${token.actorId ?? "-"} linked=${String(Boolean(token.actorLink))} (${token.x},${token.y})`
12543
+ row: (token) => `${token.id} ${token.name} actor=${token.actorId ?? "-"} linked=${String(Boolean(token.actorLink))} (${token.x},${token.y})${levelMembershipCell(token)}`
12352
12544
  }),
12353
12545
  detailRenderer(
12354
12546
  ["scene.token.get", "scene.token.create", "scene.token.update", "scene.token.clone"],
@@ -12372,7 +12564,7 @@ var RENDERERS = registerRenderers([
12372
12564
  empty: "No tiles found.",
12373
12565
  heading: "Tiles",
12374
12566
  prefix: scenePrefix,
12375
- row: (tile) => `${tile.id} (${tile.x},${tile.y}) ${tile.width}x${tile.height} hidden=${String(Boolean(tile.hidden))}`
12567
+ row: (tile) => `${tile.id} (${tile.x},${tile.y}) ${tile.width}x${tile.height} hidden=${String(Boolean(tile.hidden))}${levelMembershipCell(tile)}`
12376
12568
  }),
12377
12569
  detailRenderer(
12378
12570
  ["scene.tile.get", "scene.tile.create", "scene.tile.update", "scene.tile.clone"],
@@ -12384,7 +12576,7 @@ var RENDERERS = registerRenderers([
12384
12576
  empty: "No sounds found.",
12385
12577
  heading: "Sounds",
12386
12578
  prefix: scenePrefix,
12387
- row: (sound) => `${sound.id} ${sound.path ?? ""} (${sound.x},${sound.y}) r=${sound.radius} hidden=${String(Boolean(sound.hidden))}`
12579
+ row: (sound) => `${sound.id} ${sound.path ?? ""} (${sound.x},${sound.y}) r=${sound.radius} hidden=${String(Boolean(sound.hidden))}${levelMembershipCell(sound)}`
12388
12580
  }),
12389
12581
  detailRenderer(
12390
12582
  ["scene.sound.get", "scene.sound.create", "scene.sound.update", "scene.sound.clone"],
@@ -12396,20 +12588,32 @@ var RENDERERS = registerRenderers([
12396
12588
  empty: "No walls found.",
12397
12589
  heading: "Walls",
12398
12590
  prefix: scenePrefix,
12399
- row: (wall) => `${wall.id} ${wall.door ?? 0}/${wall.ds ?? 0}/${wall.doorSound ?? ""} c=[${(wall.c ?? []).join(",")}]`
12591
+ row: (wall) => `${wall.id} ${wall.door ?? 0}/${wall.ds ?? 0}/${wall.doorSound ?? ""} c=[${(wall.c ?? []).join(",")}]${levelMembershipCell(wall)}`
12400
12592
  }),
12401
12593
  detailRenderer(
12402
12594
  ["scene.wall.get", "scene.wall.create", "scene.wall.update", "scene.wall.clone"],
12403
12595
  (result) => renderWallDetails(result.sceneId, result.wall)
12404
12596
  ),
12405
12597
  deleteRenderer("scene.wall.delete", "wall", { suffix: sceneSuffix }),
12598
+ listRenderer("scene.level.list", {
12599
+ key: "levels",
12600
+ empty: "No levels found.",
12601
+ heading: "Levels",
12602
+ prefix: scenePrefix,
12603
+ row: (level) => renderLevelRow(level)
12604
+ }),
12605
+ detailRenderer(
12606
+ ["scene.level.get", "scene.level.create", "scene.level.update", "scene.level.clone"],
12607
+ (result) => renderLevelDetails(result.sceneId, result.level)
12608
+ ),
12609
+ deleteRenderer("scene.level.delete", "level", { suffix: sceneSuffix }),
12406
12610
  detailRenderer(BATCH_WRITE_COMMANDS, (result, command) => renderBatchWriteResult(command, result)),
12407
12611
  listRenderer("scene.note.list", {
12408
12612
  key: "notes",
12409
12613
  empty: "No notes found.",
12410
12614
  heading: "Notes",
12411
12615
  prefix: scenePrefix,
12412
- row: (note) => `${note.id} ${note.text ?? ""} (${note.x},${note.y}) ${note.texture?.src ?? ""} ${note.iconSize ?? ""}`
12616
+ row: (note) => `${note.id} ${note.text ?? ""} (${note.x},${note.y}) ${note.texture?.src ?? ""} ${note.iconSize ?? ""}${levelMembershipCell(note)}`
12413
12617
  }),
12414
12618
  detailRenderer(
12415
12619
  ["scene.note.get", "scene.note.create", "scene.note.update", "scene.note.clone"],
@@ -12421,7 +12625,7 @@ var RENDERERS = registerRenderers([
12421
12625
  empty: "No drawings found.",
12422
12626
  heading: "Drawings",
12423
12627
  prefix: scenePrefix,
12424
- row: (drawing) => `${drawing.id} ${drawing.text ?? ""} (${drawing.x},${drawing.y}) ${drawing.shape?.type ?? ""} ${String(Boolean(drawing.hidden))} ${drawing.name ?? "(no name)"}`
12628
+ row: (drawing) => `${drawing.id} ${drawing.text ?? ""} (${drawing.x},${drawing.y}) ${drawing.shape?.type ?? ""} ${String(Boolean(drawing.hidden))} ${drawing.name ?? "(no name)"}${levelMembershipCell(drawing)}`
12425
12629
  }),
12426
12630
  detailRenderer(
12427
12631
  ["scene.drawing.get", "scene.drawing.create", "scene.drawing.update", "scene.drawing.clone"],
@@ -12433,7 +12637,7 @@ var RENDERERS = registerRenderers([
12433
12637
  empty: "No lights found.",
12434
12638
  heading: "Lights",
12435
12639
  prefix: scenePrefix,
12436
- row: (light) => `${light.id} (${light.x},${light.y}) ${light.config?.dim ?? ""}/${light.config?.bright ?? ""} ${String(Boolean(light.hidden))} ${light.name ?? "(no name)"}`
12640
+ row: (light) => `${light.id} (${light.x},${light.y}) ${light.config?.dim ?? ""}/${light.config?.bright ?? ""} ${String(Boolean(light.hidden))} ${light.name ?? "(no name)"}${levelMembershipCell(light)}`
12437
12641
  }),
12438
12642
  detailRenderer(
12439
12643
  ["scene.light.get", "scene.light.create", "scene.light.update", "scene.light.clone"],
@@ -12457,7 +12661,7 @@ var RENDERERS = registerRenderers([
12457
12661
  empty: "No regions found.",
12458
12662
  heading: "Regions",
12459
12663
  prefix: scenePrefix,
12460
- row: (region) => `${region.id} ${region.name ?? ""} ${region.visibility ?? ""} shapes=${region.shapesCount ?? 0} behaviors=${region.behaviorsCount ?? 0}`
12664
+ row: (region) => `${region.id} ${region.name ?? ""} ${region.visibility ?? ""} shapes=${region.shapesCount ?? 0} behaviors=${region.behaviorsCount ?? 0}${levelMembershipCell(region)}`
12461
12665
  }),
12462
12666
  detailRenderer(
12463
12667
  ["scene.region.get", "scene.region.create", "scene.region.update", "scene.region.clone"],
@@ -12856,11 +13060,17 @@ function addReservedIncludeOption(command, presence) {
12856
13060
  function addActorIncludeOption(command) {
12857
13061
  return command.option(
12858
13062
  "--include <fields>",
12859
- "Comma-separated extra fields (items.flags, items.effects, prepared add output; flags/effects are always present, so those two have no effect)",
13063
+ "Comma-separated extra fields (items adds the owned items, which items.flags and items.effects also imply; prepared adds derived data; flags/effects are always present, so those two have no effect)",
12860
13064
  parseActorIncludeFields
12861
13065
  );
12862
13066
  }
13067
+ var INITIAL_LEVEL_FIELD = {
13068
+ flag: "--initial-level <initialLevel>",
13069
+ help: "Initial level id (Foundry v14)"
13070
+ };
12863
13071
  var SCENE_FIELDS = [
13072
+ FOLDER_ID_FIELD,
13073
+ CLEAR_FOLDER_FIELD,
12864
13074
  { flag: "--active <active>", help: "Scene active state", parser: parseBoolean },
12865
13075
  { flag: "--navigation <navigation>", help: "Scene navigation visibility", parser: parseBoolean },
12866
13076
  { flag: "--nav-order <navOrder>", help: "Scene navigation order", parser: parseNumber },
@@ -12873,14 +13083,14 @@ var SCENE_FIELDS = [
12873
13083
  { flag: "--shift-y <shiftY>", help: "Background Y shift", parser: parseNumber },
12874
13084
  { flag: "--nav-name <navName>", help: "Navigation label" },
12875
13085
  { flag: "--thumb <thumb>", help: "Thumbnail image path" },
12876
- { flag: "--foreground <foreground>", help: "Foreground image path" },
13086
+ { flag: "--foreground <foreground>", help: "Foreground image path (Foundry v13)" },
12877
13087
  {
12878
13088
  flag: "--foreground-elevation <foregroundElevation>",
12879
- help: "Foreground image elevation",
13089
+ help: "Foreground image elevation (Foundry v13)",
12880
13090
  parser: parseNumber
12881
13091
  },
12882
13092
  { flag: "--sort <sort>", help: "Sort order", parser: parseNumber },
12883
- { flag: "--initial-level <initialLevel>", help: "Initial elevation level id" },
13093
+ INITIAL_LEVEL_FIELD,
12884
13094
  { flag: "--playlist <playlistId>", help: "Linked playlist id" },
12885
13095
  { flag: "--playlist-sound <playlistSoundId>", help: "Linked playlist sound id" },
12886
13096
  { flag: "--journal <journalId>", help: "Linked journal id" },
@@ -12900,14 +13110,15 @@ var SCENE_FIELDS = [
12900
13110
  {
12901
13111
  flag: "--background-json <json>",
12902
13112
  help: {
12903
- create: "Scene background as JSON object",
12904
- update: "Scene background patch as JSON object",
12905
- clone: "Scene background override as JSON object"
13113
+ create: "Scene background as JSON object (Foundry v13)",
13114
+ update: "Scene background patch as JSON object (Foundry v13)",
13115
+ clone: "Scene background override as JSON object (Foundry v13)"
12906
13116
  }
12907
13117
  }
12908
13118
  ];
12909
13119
  function addSceneFieldOptions(command, verb) {
12910
- return addFieldOptions(command, verb, SCENE_FIELDS);
13120
+ const specs = verb === "create" ? SCENE_FIELDS.filter((spec) => spec !== INITIAL_LEVEL_FIELD) : SCENE_FIELDS;
13121
+ return addFieldOptions(command, verb, specs);
12911
13122
  }
12912
13123
  var TOKEN_NAME_FIELD = {
12913
13124
  flag: "--name <name>",
@@ -14351,7 +14562,9 @@ function createDaemonControlRunner(dependencies) {
14351
14562
  `);
14352
14563
  else if (response.ok) write(dependencies.stdout, `${JSON.stringify(response.result, null, 2)}
14353
14564
  `);
14354
- else
14565
+ else write(dependencies.stderr, `${renderProtocolError(response.error)}
14566
+ `);
14567
+ if (!response.ok)
14355
14568
  throw new CommanderError4(
14356
14569
  exitCodeForErrorCode(response.error?.code),
14357
14570
  "fvtt-world-cli.remoteError",
@@ -14684,11 +14897,9 @@ function recordSkillInstalls(dependencies, roots, { remove = false } = {}) {
14684
14897
  else current.add(root);
14685
14898
  }
14686
14899
  const skillInstalls = [...current].sort();
14687
- dependencies.configStore.writeConfig(
14688
- mergePersistedCliConfig(
14689
- existing,
14690
- skillInstalls.length > 0 ? { skillInstalls } : { skillInstalls: void 0 }
14691
- )
14900
+ writeCliConfigUpdate(
14901
+ dependencies.configStore,
14902
+ skillInstalls.length > 0 ? { skillInstalls } : { skillInstalls: void 0 }
14692
14903
  );
14693
14904
  } catch (error) {
14694
14905
  const message = error instanceof Error ? error.message : String(error);
@@ -15547,12 +15758,7 @@ function registerConfig({ program, dependencies }) {
15547
15758
  });
15548
15759
  config.command("set-upload-limit").description("Persist the file.upload size limit (raw bytes, pre-base64) for the daemon and CLI").argument("<size>", "Upload size limit as bytes or a size like 100MiB (max 512MiB)").action(function setUploadLimit(size) {
15549
15760
  const uploadLimitBytes = parseUploadLimitBytes(size);
15550
- const persistedConfig = getStoredConfig(dependencies);
15551
- const nextConfig = mergePersistedCliConfig(persistedConfig, {
15552
- daemonUrl: persistedConfig?.daemonUrl ?? DEFAULT_DAEMON_URL,
15553
- uploadLimitBytes
15554
- });
15555
- dependencies.configStore.writeConfig(nextConfig);
15761
+ writeCliConfigUpdate(dependencies.configStore, { uploadLimitBytes });
15556
15762
  const payload = {
15557
15763
  configPath: dependencies.configStore.getConfigPath(),
15558
15764
  uploadLimitBytes,
@@ -16680,6 +16886,12 @@ function registerPlaylist({ program, dependencies }) {
16680
16886
 
16681
16887
  // src/commands/scene.ts
16682
16888
  import { Option as Option11 } from "commander";
16889
+ function addSceneLevelScopeOption(command) {
16890
+ return command.requiredOption("--scene-id <sceneId>", "Scene id");
16891
+ }
16892
+ function addSceneLevelTargetOptions(command, levelDescription = "Level id") {
16893
+ return addSceneLevelScopeOption(command).requiredOption("--level-id <levelId>", levelDescription);
16894
+ }
16683
16895
  function registerScene({ program, dependencies }) {
16684
16896
  const { registerOwnershipSet, registerCompendiumImport, registerBatchWriteCommands } = createSharedRegistrars(dependencies);
16685
16897
  const scene = program.command("scene").description("Foundry scene commands");
@@ -17255,6 +17467,58 @@ function registerScene({ program, dependencies }) {
17255
17467
  });
17256
17468
  });
17257
17469
  registerBatchWriteCommands(sceneWall, { prefix: "scene.wall", noun: "wall" });
17470
+ const sceneLevel = scene.command("level").description("Scene level commands (Foundry v14)");
17471
+ addPaginationOptions(addSceneLevelScopeOption(sceneLevel.command("list"))).action(
17472
+ async function listSceneLevels(options) {
17473
+ await executeRemoteCommand({
17474
+ commandName: "scene.level.list",
17475
+ params: { sceneId: options.sceneId, ...paginationParams(options) },
17476
+ command: this,
17477
+ dependencies
17478
+ });
17479
+ }
17480
+ );
17481
+ addSceneLevelTargetOptions(sceneLevel.command("get")).action(async function getSceneLevel(options) {
17482
+ await executeRemoteCommand({
17483
+ commandName: "scene.level.get",
17484
+ params: { sceneId: options.sceneId, levelId: options.levelId },
17485
+ command: this,
17486
+ dependencies
17487
+ });
17488
+ });
17489
+ addSceneLevelScopeOption(addIdempotencyKeyOption(sceneLevel.command("create"))).option("--data-json <json>", "Level data as a JSON object (name is required; e.g. elevation/background)").action(async function createSceneLevel(options) {
17490
+ await executeRemoteCommand({
17491
+ commandName: "scene.level.create",
17492
+ params: createSceneLevelCreateParams(options),
17493
+ command: this,
17494
+ dependencies
17495
+ });
17496
+ });
17497
+ addSceneLevelTargetOptions(sceneLevel.command("update")).option("--patch-json <json>", "Level patch as a JSON object (e.g. background/foreground/elevation)").action(async function updateSceneLevel(options) {
17498
+ await executeRemoteCommand({
17499
+ commandName: "scene.level.update",
17500
+ params: createSceneLevelUpdateParams(options),
17501
+ command: this,
17502
+ dependencies
17503
+ });
17504
+ });
17505
+ addSceneLevelTargetOptions(addIdempotencyKeyOption(sceneLevel.command("clone")), "Source level id").option("--patch-json <json>", "Override fields for the clone as a JSON object").action(async function cloneSceneLevel(options) {
17506
+ await executeRemoteCommand({
17507
+ commandName: "scene.level.clone",
17508
+ params: createSceneLevelCloneParams(options),
17509
+ command: this,
17510
+ dependencies
17511
+ });
17512
+ });
17513
+ addSceneLevelTargetOptions(sceneLevel.command("delete")).action(async function deleteSceneLevel(options) {
17514
+ await executeRemoteCommand({
17515
+ commandName: "scene.level.delete",
17516
+ params: { sceneId: options.sceneId, levelId: options.levelId },
17517
+ command: this,
17518
+ dependencies
17519
+ });
17520
+ });
17521
+ registerBatchWriteCommands(sceneLevel, { prefix: "scene.level", noun: "level" });
17258
17522
  const sceneNote = scene.command("note").description("Scene-embedded note (map pin) commands");
17259
17523
  addPaginationOptions(sceneNote.command("list").requiredOption("--scene-id <sceneId>", "Scene id")).action(
17260
17524
  async function listSceneNotes(options) {
@@ -18009,7 +18273,7 @@ function registerSystem({ program, dependencies }) {
18009
18273
  const system = program.command("system").description("Foundry bridge system commands");
18010
18274
  system.addHelpText(
18011
18275
  "after",
18012
- "\nResult key (--json): .result.pong + .result.bridge (ping); .result.foundry.* / .result.system.* / .result.world.* / .result.module.* / .result.modules[] / .result.bridge.* / .result.limits.* / .result.commands[] (info)."
18276
+ "\nResult key (--json): .result.pong + .result.bridge (ping); .result.foundry.* / .result.system.* / .result.world.* / .result.module.* / .result.moduleCount / .result.activeModuleCount / .result.modules[] (with --include modules) / .result.bridge.* / .result.limits.* (info)."
18013
18277
  );
18014
18278
  system.command("ping").action(async function ping() {
18015
18279
  await executeRemoteCommand({
@@ -18019,10 +18283,14 @@ function registerSystem({ program, dependencies }) {
18019
18283
  dependencies
18020
18284
  });
18021
18285
  });
18022
- system.command("info").action(async function info() {
18286
+ system.command("info").option(
18287
+ "--include <fields>",
18288
+ "Comma-separated extra fields (modules adds the installed module list)",
18289
+ parseSystemInfoIncludeFields
18290
+ ).action(async function info(options) {
18023
18291
  await executeRemoteCommand({
18024
18292
  commandName: "system.info",
18025
- params: {},
18293
+ params: options.include ? { include: options.include } : {},
18026
18294
  command: this,
18027
18295
  dependencies
18028
18296
  });
package/docs/commands.md CHANGED
@@ -29,6 +29,7 @@ Use these families to narrow your search, then check their help for supported op
29
29
  | Edit world content | `actor`, `item`, `journal`, `scene`, `macro`, `playlist`, `table`, `cards` |
30
30
  | Edit embedded content | Nested families such as `actor.item`, `playlist.sound`, `table.result`, and `*.effect` |
31
31
  | Edit scene placeables | `scene.token`, `scene.wall`, `scene.region`, and related families |
32
+ | Edit scene levels (v14) | `scene.level` |
32
33
  | Manage encounters | `combat`, `combat.combatant`, `combat.group` |
33
34
  | Organize content and access | `folder`, `*.ownership.set`, `user` |
34
35
  | Manage chat and settings | `chat`, `setting` |
@@ -63,6 +64,29 @@ fvtt-world-cli scene token list --scene-id <sceneId> --json
63
64
  fvtt-world-cli scene token get --scene-id <sceneId> --token-id <tokenId> --json
64
65
  ```
65
66
 
67
+ ### Build a scene with levels
68
+
69
+ On Foundry v14 a scene's maps live on its levels. Pass the levels when you create the scene:
70
+
71
+ ```bash
72
+ fvtt-world-cli scene create --name "Tower" --json \
73
+ --data-json '{"levels":[{"name":"Ground","background":{"src":"worlds/<worldId>/maps/ground.webp"}},
74
+ {"name":"Roof","elevation":{"bottom":20,"top":40}}]}'
75
+ fvtt-world-cli scene level list --scene-id <sceneId> --json
76
+ fvtt-world-cli scene level update --scene-id <sceneId> --level-id <levelId> \
77
+ --patch-json '{"foreground":{"src":"worlds/<worldId>/maps/roof.webp"}}' --json
78
+ ```
79
+
80
+ The first supplied level becomes the initial one. To start players on another level, run `scene
81
+ update --scene-id <sceneId> --initial-level <levelId>`; `--patch-json '{"initialLevel":null}'` goes
82
+ back to the first level. A scene keeps at least one level, so `scene level delete` and `delete-many`
83
+ refuse to remove the last one.
84
+
85
+ Deleting a level also deletes its tokens and every placeable that exists only on that level. A dry
86
+ run does not list them, so read the scene's placeables before deleting a level.
87
+
88
+ [Compatibility](compatibility.md) lists the scene map fields each Foundry version accepts.
89
+
66
90
  ### Send a batch of commands
67
91
 
68
92
  Save these lines as `requests.ndjson`:
@@ -131,6 +155,9 @@ List rows contain fewer fields than `get` results. Read the target with `get` be
131
155
  update. Reads return authored state; some commands accept `include` options for derived or expensive
132
156
  data. Check the command's help for available projections and filters.
133
157
 
158
+ Actor results carry `itemCount` in place of the owned items; `actor get` and `actor get-many`
159
+ return the items with `--include items`. `system info --include modules` lists installed modules.
160
+
134
161
  ### Updates and merge semantics
135
162
 
136
163
  Use JSON flags such as `--data-json` and `--patch-json` for structured values. Updates are patches:
@@ -207,7 +234,7 @@ inspect `auth pending`, then use `auth approve <code> --yes` for the selected re
207
234
 
208
235
  | Command | Effect |
209
236
  |---|---|
210
- | `auth status` | Show bridge state and public profile metadata |
237
+ | `auth status` | Show bridge state, the commands the connected bridge advertised, and public profile metadata |
211
238
  | `auth pending` | List pending codes, expiry, and browser/world/GM identity |
212
239
  | `auth approve [code] [--yes]` | Approve a selected request; omit the code only when exactly one is pending |
213
240
  | `auth deny <code>` | Reject a pending request |
@@ -6,6 +6,8 @@ The bridge supports Foundry VTT v13 and v14. Check your connected version with
6
6
  | Area | v13 | v14 | What to do |
7
7
  |---|---|---|---|
8
8
  | Measured templates | Available | Removed from core | Use `scene.template` commands on v13. They are unavailable on v14. |
9
+ | Scene maps | One map per scene: `background`, `backgroundColor`, `foreground`, `foregroundElevation` and `fog.overlay` on the scene | Each level has its own map; the scene has `levels` and `initialLevel` | On v14, set maps through `levels` in `scene.create` or with `scene.level` commands. The scene-level map fields, including `fog.overlay`, are refused on v14, and `scene.level`, `levels` and `initialLevel` are refused on v13. |
10
+ | Placeable levels | Placeables have no level fields | A token sits on one `level`; tiles, walls, lights, sounds, notes, drawings and regions have a `levels` list, where an empty list means every level | On v14, read `level` or `levels` from placeable results to see what a level holds before deleting it. Placeable writes that set `level` or `levels` are refused on v13. |
9
11
  | Scene thumbnail rendering | Renders the scene | Renders the scene's initial level by default | On v14, expect the initial level in the thumbnail. Check the returned dimensions and image. |
10
12
  | Scene thumbnail files | Uses a stable scene-based filename | Uses a filename based on the image content | Use the returned path. On v14, a changed image can leave an older thumbnail file behind. |
11
13
  | User permission names | `TEMPLATE_CREATE` | `REGION_CREATE` | Use the permission keys reported by the connected Foundry version when calling `user.permissions.set`. |
package/docs/protocol.md CHANGED
@@ -178,7 +178,8 @@ The protocol package exports `REQUEST_SCHEMA` and `COMMAND_RESPONSE_SCHEMA` for
178
178
  Document results use type-named keys and expose `id` as the public identifier. A source `_id`
179
179
  mirror may accompany it. List responses use smaller projections than single-document reads;
180
180
  filters apply before pagination. Previewed documents have no persistent identity, so an id
181
- observed during a preview must not be reused.
181
+ observed during a preview must not be reused. Some result fields depend on the connected Foundry
182
+ version; [Foundry compatibility](compatibility.md) lists them.
182
183
 
183
184
  Broadcast actions report `dispatched` and target users rather than a confirmed player-side result.
184
185
  The active/inactive split is included where knowable.
@@ -341,8 +342,8 @@ The wait has two phases because the decision can outlast a normal request timeou
341
342
  policy can change before dispatch.
342
343
 
343
344
  The module supplies each opaque `approvalId`; callers do not construct one. Approval request schemas
344
- are closed. `approval.await`, `approval.cancel`, and `policy.snapshot` do not appear in `commands`,
345
- `system.info` command inventory, or bridge status. `schema <command>` still returns their schemas.
345
+ are closed. `approval.await`, `approval.cancel`, and `policy.snapshot` do not appear in `commands` or
346
+ bridge status. `schema <command>` still returns their schemas.
346
347
  The bridge handshake advertises them because the daemon must forward them.
347
348
 
348
349
  ## Delivery states and retries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fvtt-world-cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Safe local CLI and authenticated bridge for automating live Foundry VTT worlds.",
5
5
  "keywords": [
6
6
  "foundry-vtt",
@@ -56,6 +56,6 @@
56
56
  "zod": "^3.24.3"
57
57
  },
58
58
  "devDependencies": {
59
- "@fvtt-world-cli/protocol": "1.1.2"
59
+ "@fvtt-world-cli/protocol": "1.2.0"
60
60
  }
61
61
  }
@@ -75,6 +75,11 @@ A macro that throws fails `macro.execute` with a partial outcome — read what i
75
75
  retrying — while a macro that catches its own errors still returns `null`, which proves nothing;
76
76
  verify effects with reads, and treat `MACRO_TIMEOUT` as indeterminate — the macro may still be
77
77
  running.
78
+ On Foundry v14 maps live on levels: pass `levels` in `scene.create`, then use `scene.level.*`;
79
+ level ids in a preview are provisional.
80
+ Deleting a level also deletes its tokens and every placeable only on that level, and the dry run
81
+ does not list them: read the scene's placeables (token `level`, others' `levels`) and tell the user
82
+ what the delete removes before running it.
78
83
  `setting.set` cannot touch this module's own namespace (`SETTING_PROTECTED` is final; never retry).
79
84
  When a setting write returns `requiresReload: true`, the change needs a GM-client reload to take
80
85
  effect; `system.reload` performs it, drops the bridge, and requires a reconnect wait before the
@@ -135,7 +140,8 @@ details}}`, alongside `protocolVersion`, `type`, and the correlation `id` (echoi
135
140
  Documents live under a type-named key inside `result` that varies between commands but is stable
136
141
  for each one. `id` is the public identifier; a `_id` mirror may accompany it. Values requested
137
142
  through `include` flags may be derived or version-dependent. Write them back only when the
138
- update schema explicitly accepts them.
143
+ update schema explicitly accepts them. Actor reads return `itemCount` in place of owned items; add
144
+ `--include items` when the task needs them, or use `actor item list` and `actor item get`.
139
145
 
140
146
  ## Retry classification
141
147
 
@@ -1 +1 @@
1
- d1757739e55e01589814ed919912bbe5423afa99aee16a1422fbaee5bed11c8a
1
+ 248c52464e776236de6f937530e220dcb77b45a8b5cc754adbb4cfb4671a8c66