@craftrpgs/cli 0.2.0-alpha.0 → 0.2.0-alpha.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.
Files changed (2) hide show
  1. package/dist/bin.js +623 -19
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -26899,6 +26899,7 @@ End each reply on something concrete in front of the player's character that the
26899
26899
  ];
26900
26900
  var RESPONSE_LENGTH = "Response length";
26901
26901
  var ROLL_NUMBERS = "Roll numbers in narration";
26902
+ var LONG_TRAVEL = "Long-distance travel";
26902
26903
  var PLATFORM_OPTIONS = [
26903
26904
  {
26904
26905
  revision: 3,
@@ -26984,6 +26985,46 @@ var PLATFORM_OPTIONS = [
26984
26985
  }
26985
26986
  ]
26986
26987
  }
26988
+ },
26989
+ {
26990
+ revision: 1,
26991
+ option: {
26992
+ key: "platform-long-distance-travel",
26993
+ label: LONG_TRAVEL,
26994
+ description: "What happens when the party travels somewhere far away.",
26995
+ kind: "one",
26996
+ default: ["up-to-gm"],
26997
+ choices: [
26998
+ {
26999
+ key: "up-to-gm",
27000
+ label: "Up to the GM",
27001
+ description: "The Game Master decides when the road is worth playing.",
27002
+ instructions: []
27003
+ },
27004
+ {
27005
+ key: "fast-travel",
27006
+ label: "Fast travel",
27007
+ description: "Skip ahead to the destination, with the trip summed up.",
27008
+ instructions: [
27009
+ {
27010
+ when: "always",
27011
+ body: `When the party travels somewhere far (a trip of hours or more), take them straight there in the same reply: record it as arriving ("Move Kael and Mira to Harrowmere"), never "set out for", sum up the trip in a sentence or two (how long it took, how the way went), and go on from the arrival. When the player set out from the map, tell the arrival the same way.`
27012
+ }
27013
+ ]
27014
+ },
27015
+ {
27016
+ key: "journey",
27017
+ label: "Journey",
27018
+ description: "Travel there step by step, with the road played out.",
27019
+ instructions: [
27020
+ {
27021
+ when: "always",
27022
+ body: `When the party travels somewhere far (a trip of hours or more), play it as a journey to where the player said they are going. Set out first ("Kael and Mira set out for Harrowmere by the coast road; it takes about three days"), then tell the way in stages over several replies, for as long as the distance really takes: the road and the weather, rests and camps, people and trouble met on the way, with time passing as it would ("It is now dusk"). The party stays on the way the whole time: a camp or a rest is told with the time it takes, never a move to a camp or a stretch of road. Someone met stops the journey where they meet; when the party goes on from there, record it as "Kael and Mira set out again for Harrowmere", never as a move farther along the road. Keep heading for the place the player named unless they change course, and record the arrival ("Move Kael and Mira to Harrowmere") only once the way has been travelled. When the player set out from the map, they are already on the way: go on with the journey.`
27023
+ }
27024
+ ]
27025
+ }
27026
+ ]
27027
+ }
26987
27028
  }
26988
27029
  ];
26989
27030
 
@@ -41012,6 +41053,8 @@ var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V6 = "file-designation-metadata:v6"
41012
41053
  var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V7 = "file-designation-metadata:v7";
41013
41054
  var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V8 = "file-designation-metadata:v8";
41014
41055
  var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9 = "file-designation-metadata:v9";
41056
+ var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V10 = "file-designation-metadata:v10";
41057
+ var FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V11 = "file-designation-metadata:v11";
41015
41058
  var GAME_START_OPENING_MAX_LENGTH = 1e4;
41016
41059
  var GAME_START_MAX_OPENING_BEATS = 50;
41017
41060
  var GAME_START_MAX_CHARACTER_OPENINGS = 100;
@@ -41212,6 +41255,142 @@ var MapDataV6Schema = MapDataV5Schema.extend({
41212
41255
  var MapDataV8Schema = MapDataV6Schema.extend({
41213
41256
  grid: MapGridSchema.nullable().default(null)
41214
41257
  });
41258
+ var MAP_EXIT_KINDS = [
41259
+ "door",
41260
+ "gate",
41261
+ "stairs",
41262
+ "ladder",
41263
+ "hatch",
41264
+ "path",
41265
+ "road",
41266
+ "dock",
41267
+ "portal",
41268
+ "other"
41269
+ ];
41270
+ var MAP_EXIT_DIRECTIONS = [
41271
+ "up",
41272
+ "down",
41273
+ "in",
41274
+ "out",
41275
+ "through"
41276
+ ];
41277
+ var MapExitSchema = exports_external.object({
41278
+ kind: exports_external.enum(MAP_EXIT_KINDS).default("door"),
41279
+ to: exports_external.string().uuid().nullable().default(null),
41280
+ direction: exports_external.enum(MAP_EXIT_DIRECTIONS).optional(),
41281
+ arriveAt: exports_external.string().uuid().nullable().optional(),
41282
+ main: exports_external.boolean().optional(),
41283
+ locked: exports_external.boolean().optional(),
41284
+ hidden: exports_external.boolean().optional()
41285
+ });
41286
+ var MapAreaV10Schema = MapAreaSchema.extend({
41287
+ exit: MapExitSchema.optional()
41288
+ });
41289
+ var MapPointV10Schema = MapPointSchema.extend({
41290
+ exit: MapExitSchema.optional()
41291
+ });
41292
+ var MapDataV10Schema = MapDataV8Schema.extend({
41293
+ areas: exports_external.array(MapAreaV10Schema),
41294
+ points: exports_external.array(MapPointV10Schema)
41295
+ });
41296
+ var COMPASS_DIRECTIONS = [
41297
+ "north",
41298
+ "northeast",
41299
+ "east",
41300
+ "southeast",
41301
+ "south",
41302
+ "southwest",
41303
+ "west",
41304
+ "northwest"
41305
+ ];
41306
+ var PLACE_WAY_KINDS = [
41307
+ ...MAP_EXIT_KINDS,
41308
+ "bridge",
41309
+ "ferry",
41310
+ "tunnel",
41311
+ "ritual"
41312
+ ];
41313
+ var PLACE_WAY_DIRECTIONS = [
41314
+ ...MAP_EXIT_DIRECTIONS,
41315
+ ...COMPASS_DIRECTIONS
41316
+ ];
41317
+ var relationWords = (max) => exports_external.string().trim().min(1).max(max);
41318
+ var PlaceWaySchema = MapExitSchema.extend({
41319
+ kind: exports_external.enum(PLACE_WAY_KINDS).default("door"),
41320
+ direction: exports_external.enum(PLACE_WAY_DIRECTIONS).optional(),
41321
+ oneWay: exports_external.boolean().optional(),
41322
+ when: relationWords(200).optional(),
41323
+ requires: relationWords(200).optional(),
41324
+ by: exports_external.string().uuid().optional(),
41325
+ label: relationWords(120).optional(),
41326
+ distance: relationWords(60).optional(),
41327
+ time: relationWords(60).optional()
41328
+ });
41329
+ var MapAreaV11Schema = MapAreaSchema.extend({
41330
+ exit: PlaceWaySchema.optional()
41331
+ });
41332
+ var MapPointV11Schema = MapPointSchema.extend({
41333
+ exit: PlaceWaySchema.optional()
41334
+ });
41335
+ var MapDataV11Schema = MapDataV8Schema.extend({
41336
+ areas: exports_external.array(MapAreaV11Schema),
41337
+ points: exports_external.array(MapPointV11Schema)
41338
+ });
41339
+ var RelationIdSchema = exports_external.string().uuid();
41340
+ var RelationTargetSchema = exports_external.string().uuid();
41341
+ var InsideRelationSchema = exports_external.object({
41342
+ id: RelationIdSchema,
41343
+ type: exports_external.literal("inside"),
41344
+ to: RelationTargetSchema,
41345
+ level: exports_external.number().int().min(-100).max(500).optional()
41346
+ });
41347
+ var ConnectsRelationSchema = PlaceWaySchema.extend({
41348
+ id: RelationIdSchema,
41349
+ type: exports_external.literal("connects")
41350
+ });
41351
+ var OtherRelationSchema = exports_external.object({
41352
+ id: RelationIdSchema,
41353
+ type: exports_external.literal("other"),
41354
+ to: RelationTargetSchema,
41355
+ label: relationWords(120)
41356
+ });
41357
+ var PlaceRelationSchema = exports_external.discriminatedUnion("type", [
41358
+ InsideRelationSchema,
41359
+ ConnectsRelationSchema,
41360
+ OtherRelationSchema
41361
+ ]);
41362
+ var MAX_PLACE_RELATIONS = 100;
41363
+ var TRANSPORT_KINDS = [
41364
+ "ship",
41365
+ "boat",
41366
+ "airship",
41367
+ "wagon",
41368
+ "caravan",
41369
+ "carriage",
41370
+ "train",
41371
+ "car",
41372
+ "bus",
41373
+ "plane",
41374
+ "mount",
41375
+ "other"
41376
+ ];
41377
+ var TRANSPORT_MEDIUMS = [
41378
+ "water",
41379
+ "land",
41380
+ "air",
41381
+ "rail",
41382
+ "underground",
41383
+ "space"
41384
+ ];
41385
+ var TransportMetadataSchema = exports_external.object({
41386
+ kind: exports_external.enum(TRANSPORT_KINDS),
41387
+ travels: exports_external.enum(TRANSPORT_MEDIUMS).optional(),
41388
+ speed: exports_external.object({
41389
+ value: exports_external.number().positive(),
41390
+ unit: exports_external.enum(["mph", "km/h", "knots"])
41391
+ }).optional(),
41392
+ capacity: exports_external.number().int().positive().optional()
41393
+ });
41215
41394
  var GmInstructionTriggerPhaseSchema = exports_external.enum(GM_INSTRUCTION_TRIGGER_PHASES);
41216
41395
  var GmInstructionToolEventSchema = exports_external.enum(GM_INSTRUCTION_TOOL_EVENTS);
41217
41396
  var GmInstructionNarrativeMetadataSchema = exports_external.object({
@@ -41403,24 +41582,36 @@ var FileDesignationMetadataV9Schema = FileDesignationMetadataV8Schema.extend({
41403
41582
  schemaVersionId: exports_external.literal(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9).default(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9),
41404
41583
  script: ScriptMetadataSchema.optional()
41405
41584
  });
41406
- var MapSchema = FileDesignationMetadataV9Schema.shape.map.unwrap();
41407
- var LocationDesignationMetadataSchema = FileDesignationMetadataV9Schema.pick({
41585
+ var FileDesignationMetadataV10Schema = FileDesignationMetadataV9Schema.extend({
41586
+ schemaVersionId: exports_external.literal(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V10).default(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V10),
41587
+ map: MapDataV10Schema.optional(),
41588
+ transport: TransportMetadataSchema.optional()
41589
+ });
41590
+ var FileDesignationMetadataV11Schema = FileDesignationMetadataV10Schema.extend({
41591
+ schemaVersionId: exports_external.literal(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V11).default(FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V11),
41592
+ map: MapDataV11Schema.optional(),
41593
+ relations: exports_external.array(PlaceRelationSchema).max(MAX_PLACE_RELATIONS).optional()
41594
+ });
41595
+ var MapSchema = FileDesignationMetadataV11Schema.shape.map.unwrap();
41596
+ var LocationDesignationMetadataSchema = FileDesignationMetadataV11Schema.pick({
41408
41597
  schemaVersionId: true,
41409
- map: true
41598
+ map: true,
41599
+ transport: true,
41600
+ relations: true
41410
41601
  });
41411
- var CharacterDesignationMetadataSchema = FileDesignationMetadataV9Schema.pick({
41602
+ var CharacterDesignationMetadataSchema = FileDesignationMetadataV11Schema.pick({
41412
41603
  schemaVersionId: true,
41413
41604
  playable: true
41414
41605
  });
41415
- var GmInstructionDesignationMetadataSchema = FileDesignationMetadataV9Schema.pick({
41606
+ var GmInstructionDesignationMetadataSchema = FileDesignationMetadataV11Schema.pick({
41416
41607
  schemaVersionId: true,
41417
41608
  gmInstruction: true
41418
41609
  });
41419
- var GameStartDesignationMetadataSchema = FileDesignationMetadataV9Schema.pick({
41610
+ var GameStartDesignationMetadataSchema = FileDesignationMetadataV11Schema.pick({
41420
41611
  schemaVersionId: true,
41421
41612
  gameStart: true
41422
41613
  });
41423
- var ScriptDesignationMetadataSchema = FileDesignationMetadataV9Schema.pick({
41614
+ var ScriptDesignationMetadataSchema = FileDesignationMetadataV11Schema.pick({
41424
41615
  schemaVersionId: true,
41425
41616
  script: true
41426
41617
  });
@@ -41428,10 +41619,17 @@ function upgradeFileDesignationMetadata(input) {
41428
41619
  if (input === null || input === undefined) {
41429
41620
  return null;
41430
41621
  }
41431
- if (readSchemaVersionId(input) === FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9) {
41432
- return FileDesignationMetadataV9Schema.parse(input);
41622
+ const schemaVersionId = readSchemaVersionId(input);
41623
+ if (schemaVersionId === FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V11) {
41624
+ return FileDesignationMetadataV11Schema.parse(input);
41433
41625
  }
41434
- return upgradeV8MetadataToV9(upgradeMetadataToV8(input));
41626
+ if (schemaVersionId === FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V10) {
41627
+ return upgradeV10MetadataToV11(FileDesignationMetadataV10Schema.parse(input));
41628
+ }
41629
+ if (schemaVersionId === FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9) {
41630
+ return upgradeV10MetadataToV11(upgradeV9MetadataToV10(FileDesignationMetadataV9Schema.parse(input)));
41631
+ }
41632
+ return upgradeV10MetadataToV11(upgradeV9MetadataToV10(upgradeV8MetadataToV9(upgradeMetadataToV8(input))));
41435
41633
  }
41436
41634
  function upgradeMetadataToV8(input) {
41437
41635
  const schemaVersionId = readSchemaVersionId(input);
@@ -41518,6 +41716,18 @@ function upgradeV8MetadataToV9(metadata) {
41518
41716
  schemaVersionId: FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V9
41519
41717
  });
41520
41718
  }
41719
+ function upgradeV9MetadataToV10(metadata) {
41720
+ return FileDesignationMetadataV10Schema.parse({
41721
+ ...metadata,
41722
+ schemaVersionId: FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V10
41723
+ });
41724
+ }
41725
+ function upgradeV10MetadataToV11(metadata) {
41726
+ return FileDesignationMetadataV11Schema.parse({
41727
+ ...metadata,
41728
+ schemaVersionId: FILE_DESIGNATION_METADATA_SCHEMA_VERSION_V11
41729
+ });
41730
+ }
41521
41731
  function normalizeLegacyDefaultGrid(map2) {
41522
41732
  if (map2.grid?.mode !== "default") {
41523
41733
  return map2;
@@ -45379,7 +45589,7 @@ async function refreshWorkspaceScriptTypes({
45379
45589
  // package.json
45380
45590
  var package_default = {
45381
45591
  name: "@craftrpgs/cli",
45382
- version: "0.2.0-alpha.0",
45592
+ version: "0.2.0-alpha.1",
45383
45593
  description: "Sync Craft projects with a local folder — clone, edit with any coding agent, push back.",
45384
45594
  license: "UNLICENSED",
45385
45595
  homepage: "https://craftrpgs.com",
@@ -45439,6 +45649,17 @@ var cliMetaWriteResponseSchema = exports_external.object({
45439
45649
  version: exports_external.object({ id: exports_external.string().min(1) }),
45440
45650
  linkStateChanged: exports_external.boolean().optional()
45441
45651
  });
45652
+ var cliPlacePairWriteResponseSchema = exports_external.object({
45653
+ changedFileIds: exports_external.array(exports_external.string().min(1))
45654
+ });
45655
+ var cliLinkedLocationsResponseSchema = exports_external.object({
45656
+ parent: exports_external.object({ referenceId: exports_external.string() }).nullable(),
45657
+ places: exports_external.array(exports_external.object({
45658
+ referenceId: exports_external.string(),
45659
+ name: exports_external.string(),
45660
+ pair: exports_external.record(exports_external.string(), exports_external.unknown())
45661
+ }))
45662
+ });
45442
45663
  var cliFileMetaResponseSchema = exports_external.object({
45443
45664
  currentVersion: exports_external.object({
45444
45665
  id: exports_external.string().min(1),
@@ -45829,6 +46050,40 @@ function createCliClient({
45829
46050
  async putGmInstruction({ fileId, gmInstruction }) {
45830
46051
  return await putMetadata(`/api/files/${fileId}/gm-instruction`, { gmInstruction }, "GM instruction trigger save");
45831
46052
  },
46053
+ async fetchLinkedLocations({ fileId }) {
46054
+ const response = await request(`/api/files/${fileId}/relations`);
46055
+ if (response.status === 404) {
46056
+ return null;
46057
+ }
46058
+ if (!response.ok) {
46059
+ const { code, message, details } = await readErrorBody(response);
46060
+ throw new CliApiError({
46061
+ message,
46062
+ code,
46063
+ status: response.status,
46064
+ exitCode: 1,
46065
+ details
46066
+ });
46067
+ }
46068
+ return await parseJson(response, cliLinkedLocationsResponseSchema, "linked locations");
46069
+ },
46070
+ async putFileTransport({ fileId, transport }) {
46071
+ return await putMetadata(`/api/files/${fileId}/transport`, { transport }, "transport save");
46072
+ },
46073
+ async putPlacePair({ fileId, otherFileId, pair }) {
46074
+ const response = await request(`/api/files/${fileId}/relations/${otherFileId}`, pair === null ? { method: "DELETE" } : { method: "PUT", body: JSON.stringify(pair) });
46075
+ if (!response.ok) {
46076
+ const { code, message, details } = await readErrorBody(response);
46077
+ throw new CliApiError({
46078
+ message,
46079
+ code,
46080
+ status: response.status,
46081
+ exitCode: response.status === 400 || response.status === 422 ? 2 : 1,
46082
+ details
46083
+ });
46084
+ }
46085
+ return await parseJson(response, cliPlacePairWriteResponseSchema, "linked location save");
46086
+ },
45832
46087
  async putDesignationFlag({ fileId, flag, value }) {
45833
46088
  return await putMetadata(`/api/files/${fileId}/designation`, { flag, value }, "designation flag save");
45834
46089
  },
@@ -48549,11 +48804,66 @@ async function runLogout(argv) {
48549
48804
 
48550
48805
  // src/commands/map.ts
48551
48806
  import { readFile as readFile11 } from "node:fs/promises";
48807
+
48808
+ // src/commands/place-metadata.ts
48809
+ function pathOrId(ledger, referenceId) {
48810
+ return referenceIdToPath({ ledger, referenceId }) ?? referenceId;
48811
+ }
48812
+ function exitForOutput(ledger, exit) {
48813
+ return {
48814
+ ...exit,
48815
+ ...typeof exit.to === "string" ? { to: pathOrId(ledger, exit.to) } : {},
48816
+ ...typeof exit.by === "string" ? { by: pathOrId(ledger, exit.by) } : {}
48817
+ };
48818
+ }
48819
+ function relationsForOutput(ledger, relations) {
48820
+ return relations.map((relation) => exitForOutput(ledger, relation));
48821
+ }
48822
+ function listMapExitsForOutput(ledger, map2) {
48823
+ return [...map2.areas, ...map2.points].flatMap((element) => element.exit ? [
48824
+ {
48825
+ element: element.label ?? element.id,
48826
+ ...exitForOutput(ledger, element.exit)
48827
+ }
48828
+ ] : []);
48829
+ }
48830
+ function resolveExitInput({
48831
+ ledger,
48832
+ exit,
48833
+ where,
48834
+ failures
48835
+ }) {
48836
+ if (typeof exit !== "object" || exit === null || Array.isArray(exit)) {
48837
+ return exit;
48838
+ }
48839
+ const record2 = { ...exit };
48840
+ for (const key of ["to", "by"]) {
48841
+ const value = record2[key];
48842
+ if (typeof value !== "string") {
48843
+ continue;
48844
+ }
48845
+ const resolution = resolveLink({ ledger, link: value });
48846
+ if (resolution.status === "resolved") {
48847
+ record2[key] = resolution.referenceId;
48848
+ } else {
48849
+ failures.push({
48850
+ where: `${where}.${key}`,
48851
+ link: value,
48852
+ reason: resolution.status
48853
+ });
48854
+ }
48855
+ }
48856
+ return record2;
48857
+ }
48858
+
48859
+ // src/commands/map.ts
48552
48860
  var MAP_USAGE = `Usage: craft map get <location-path> [--json]
48553
48861
  ` + ` craft map set <location-path> --file <map.json>
48554
48862
  ` + `
48555
- ` + "The map JSON is the stored MapData shape; in `set` input, areas/points may\n" + 'use `"link": "<workspace path | slug.type | referenceId>"` instead of\n' + `fileReferenceId, and element ids are minted when omitted. Read
48556
- ` + ".craft/instructions/authoring-maps.md for the format and quality bar.";
48863
+ ` + "The map JSON is the stored MapData shape; in `set` input, areas/points may\n" + 'use `"link": "<workspace path | slug.type | referenceId>"` instead of\n' + `fileReferenceId, and element ids are minted when omitted. An element's
48864
+ ` + '`exit` (a door, stairs, a road to another location) takes `"to"` as a\n' + "workspace path too; no `to` is the way out to the parent map. `get` prints\n" + `links and exit targets as workspace paths. Read
48865
+ ` + `.craft/instructions/authoring-maps.md for the format and
48866
+ ` + ".craft/instructions/linking-locations.md for exits.";
48557
48867
  async function runMap(argv) {
48558
48868
  const [subcommand, ...rest] = argv;
48559
48869
  if (subcommand === "get") {
@@ -48575,16 +48885,22 @@ function hydrateMapForOutput(workspace, map2) {
48575
48885
  if (typeof element !== "object" || element === null) {
48576
48886
  return element;
48577
48887
  }
48578
- const record2 = element;
48888
+ let record2 = element;
48889
+ if (typeof record2.exit === "object" && record2.exit !== null) {
48890
+ record2 = {
48891
+ ...record2,
48892
+ exit: exitForOutput(workspace.ledger, record2.exit)
48893
+ };
48894
+ }
48579
48895
  if (typeof record2.fileReferenceId !== "string") {
48580
- return element;
48896
+ return record2;
48581
48897
  }
48582
48898
  const path = referenceIdToPath({
48583
48899
  ledger: workspace.ledger,
48584
48900
  referenceId: record2.fileReferenceId
48585
48901
  });
48586
48902
  if (!path) {
48587
- return element;
48903
+ return record2;
48588
48904
  }
48589
48905
  const { fileReferenceId: _dropped, ...rest } = record2;
48590
48906
  return { ...rest, link: path };
@@ -48677,6 +48993,14 @@ function normalizeMapInput(workspace, raw) {
48677
48993
  if (record2.fileReferenceId === undefined) {
48678
48994
  record2.fileReferenceId = null;
48679
48995
  }
48996
+ if (record2.exit !== undefined) {
48997
+ record2.exit = resolveExitInput({
48998
+ ledger: workspace.ledger,
48999
+ exit: record2.exit,
49000
+ where: `${kindLabel2}[${index}].exit`,
49001
+ failures
49002
+ });
49003
+ }
48680
49004
  for (const [key, value] of Object.entries(defaults)) {
48681
49005
  if (record2[key] === undefined) {
48682
49006
  record2[key] = value;
@@ -48945,13 +49269,75 @@ function translateGameStartAuthoringInput({
48945
49269
  return input;
48946
49270
  }
48947
49271
 
49272
+ // ../shared/src/place-pairs.ts
49273
+ var PLACE_PAIR_KINDS = [
49274
+ "same_place",
49275
+ "contains",
49276
+ "part_of",
49277
+ "separate",
49278
+ "other"
49279
+ ];
49280
+ var PairWaySchema = PlaceWaySchema.omit({
49281
+ to: true,
49282
+ arriveAt: true,
49283
+ main: true,
49284
+ oneWay: true
49285
+ }).extend({
49286
+ oneWay: exports_external.enum(["there", "back"]).optional()
49287
+ });
49288
+ var PairWordsSchema = exports_external.object({
49289
+ text: exports_external.string().trim().min(1).max(120),
49290
+ first: exports_external.enum(["a", "b"])
49291
+ });
49292
+ var PlacePairSchema = exports_external.object({
49293
+ kind: exports_external.enum(PLACE_PAIR_KINDS),
49294
+ parentRef: exports_external.string().uuid().optional(),
49295
+ way: PairWaySchema.nullable(),
49296
+ words: PairWordsSchema.nullable()
49297
+ });
49298
+ function placePairProblem({
49299
+ pair,
49300
+ aRef,
49301
+ bRef
49302
+ }) {
49303
+ if (aRef === bRef) {
49304
+ return "A location can't be linked to itself.";
49305
+ }
49306
+ if (pair.kind === "same_place") {
49307
+ if (!pair.parentRef) {
49308
+ return "Pick the location they're both in.";
49309
+ }
49310
+ if (pair.parentRef === aRef || pair.parentRef === bRef) {
49311
+ return "A location can't be inside itself.";
49312
+ }
49313
+ } else if (pair.parentRef) {
49314
+ return "Only locations in one location name the location they're in.";
49315
+ }
49316
+ if (pair.kind === "separate" && !pair.way) {
49317
+ return "Separate locations need a way between them.";
49318
+ }
49319
+ if (pair.kind === "other" && !pair.words) {
49320
+ return "Say how they relate.";
49321
+ }
49322
+ if (pair.kind !== "other" && pair.words) {
49323
+ return "Only something else has words of its own.";
49324
+ }
49325
+ return null;
49326
+ }
49327
+
48948
49328
  // src/commands/meta.ts
49329
+ var PAIR_KIND_FIELD_RE = /^kind/;
48949
49330
  var META_USAGE = `Usage: craft meta get <path> [--json]
48950
49331
  ` + ` craft meta set-game-start <game-start-path> --file <json>
48951
49332
  ` + ` craft meta set-gm-trigger <gm-instruction-path> (--file <json> | --clear)
48952
49333
  ` + ` craft meta set-playable <character-path> <true|false>
49334
+ ` + ` craft meta set-transport <location-path> (--file <json> | --clear)
49335
+ ` + ` craft meta link <location-path> <other-location-path> --file <json>
49336
+ ` + ` craft meta unlink <location-path> <other-location-path>
48953
49337
  ` + `
48954
- ` + "See .craft/instructions/making-it-playable.md for the JSON shapes.";
49338
+ ` + `See .craft/instructions/making-it-playable.md for game starts, triggers and
49339
+ ` + `playable, and .craft/instructions/linking-locations.md for transports and
49340
+ ` + "linked locations. `get` on a location shows its transport, its linked\n" + "locations (read from both ends) and its map's exits, with workspace paths.";
48955
49341
  async function runMeta(argv) {
48956
49342
  const [subcommand, ...rest] = argv;
48957
49343
  if (subcommand === "get") {
@@ -48966,6 +49352,15 @@ async function runMeta(argv) {
48966
49352
  if (subcommand === "set-playable") {
48967
49353
  return await runSetPlayable(rest);
48968
49354
  }
49355
+ if (subcommand === "set-transport") {
49356
+ return await runSetTransport(rest);
49357
+ }
49358
+ if (subcommand === "link") {
49359
+ return await runLink(rest, "link");
49360
+ }
49361
+ if (subcommand === "unlink") {
49362
+ return await runLink(rest, "unlink");
49363
+ }
48969
49364
  throw new CliError(META_USAGE, 2);
48970
49365
  }
48971
49366
  async function readJsonFile2(inputPath) {
@@ -49028,6 +49423,8 @@ async function runMetaGet(argv) {
49028
49423
  if (gameStart) {
49029
49424
  gameStart.startingLocationFileReferenceId = undefined;
49030
49425
  }
49426
+ const isLocation = meta3.fileType?.designation === "location" || Boolean(parsed?.map || parsed?.transport || parsed?.relations);
49427
+ const linkedLocations = isLocation ? await linkedLocationsForOutput(workspace, client, entry.fileId) : undefined;
49031
49428
  printJson({
49032
49429
  path,
49033
49430
  fileType: entry.fileType,
@@ -49039,11 +49436,38 @@ async function runMetaGet(argv) {
49039
49436
  areas: parsed.map.areas.length,
49040
49437
  points: parsed.map.points.length,
49041
49438
  useParentMap: parsed.map.useParentMap ?? false,
49439
+ exits: listMapExitsForOutput(workspace.ledger, parsed.map),
49042
49440
  hint: "Full map via `craft map get`."
49043
- } : null
49441
+ } : null,
49442
+ ...isLocation ? {
49443
+ transport: parsed?.transport ?? null,
49444
+ relations: relationsForOutput(workspace.ledger, parsed?.relations ?? []),
49445
+ ...linkedLocations === undefined ? {} : { linkedLocations }
49446
+ } : {}
49044
49447
  });
49045
49448
  return 0;
49046
49449
  }
49450
+ async function linkedLocationsForOutput(workspace, client, fileId) {
49451
+ const linked = await client.fetchLinkedLocations({ fileId });
49452
+ if (!linked) {
49453
+ return;
49454
+ }
49455
+ return linked.places.map((place) => {
49456
+ const pair = place.pair;
49457
+ return {
49458
+ location: pathForReference(workspace, place.referenceId) ?? place.name,
49459
+ relation: pair.kind,
49460
+ ...pair.parentRef ? { in: pathForReference(workspace, pair.parentRef) } : {},
49461
+ ...pair.way ? {
49462
+ way: exitForOutput(workspace.ledger, pair.way)
49463
+ } : {},
49464
+ ...pair.words ? {
49465
+ words: pair.words.text,
49466
+ wordsFirst: pair.words.first === "a" ? "location" : "other"
49467
+ } : {}
49468
+ };
49469
+ });
49470
+ }
49047
49471
  function projectOpeningForOutput(workspace, opening) {
49048
49472
  if (opening.kind !== "static" || !opening.beats?.length) {
49049
49473
  return opening;
@@ -49220,6 +49644,159 @@ async function runSetPlayable(argv) {
49220
49644
  printLines([`Set playable: ${value} on ${path}.`]);
49221
49645
  return 0;
49222
49646
  }
49647
+ async function runSetTransport(argv) {
49648
+ const args = parseCommandArgs({
49649
+ argv,
49650
+ flags: {
49651
+ file: { type: "string" },
49652
+ clear: { type: "boolean" },
49653
+ json: { type: "boolean" }
49654
+ },
49655
+ maxPositionals: 1
49656
+ });
49657
+ const path = args.positionals[0];
49658
+ const inputPath = stringFlag(args, "file");
49659
+ const clear = args.booleans.has("clear");
49660
+ const exactlyOneMode = clear ? !inputPath : Boolean(inputPath);
49661
+ if (!(path && exactlyOneMode)) {
49662
+ throw new CliError(META_USAGE, 2);
49663
+ }
49664
+ let transport = null;
49665
+ if (inputPath) {
49666
+ const parsed = LocationDesignationMetadataSchema.shape.transport.unwrap().safeParse(await readJsonFile2(inputPath));
49667
+ if (!parsed.success) {
49668
+ throw new CliError(`The transport JSON failed validation:
49669
+ ` + parsed.error.issues.map((issue3) => ` ${issue3.path.join(".") || "(root)"}: ${issue3.message}`).join(`
49670
+ `), 2);
49671
+ }
49672
+ transport = parsed.data;
49673
+ }
49674
+ const workspace = await loadWorkspaceContext();
49675
+ const entry = resolveTrackedEntry({ ledger: workspace.ledger, input: path });
49676
+ const client = await workspaceClient(workspace);
49677
+ const result = await client.putFileTransport({
49678
+ fileId: entry.fileId,
49679
+ transport
49680
+ });
49681
+ await freshenLedgerAfterMetaWrite({
49682
+ root: workspace.root,
49683
+ ledger: workspace.ledger,
49684
+ fileId: entry.fileId,
49685
+ versionId: result.version.id
49686
+ });
49687
+ if (args.booleans.has("json")) {
49688
+ printJson({ path, saved: true, transport });
49689
+ return 0;
49690
+ }
49691
+ printLines([
49692
+ transport ? `Saved transport on ${path}: ${transport.kind}${transport.travels ? ` (${transport.travels})` : ""}.` : `Cleared transport on ${path}.`
49693
+ ]);
49694
+ return 0;
49695
+ }
49696
+ function translatePlacePairInput({
49697
+ workspace,
49698
+ raw,
49699
+ aRef,
49700
+ bRef
49701
+ }) {
49702
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
49703
+ throw new CliError("The link JSON must be a single object.", 2);
49704
+ }
49705
+ const input = raw;
49706
+ const failures = [];
49707
+ let parentRef;
49708
+ if (typeof input.in === "string") {
49709
+ const resolution = resolveLink({
49710
+ ledger: workspace.ledger,
49711
+ link: input.in
49712
+ });
49713
+ if (resolution.status === "resolved") {
49714
+ parentRef = resolution.referenceId;
49715
+ } else {
49716
+ failures.push({ where: "in", link: input.in, reason: resolution.status });
49717
+ }
49718
+ }
49719
+ const way = input.way === undefined || input.way === null ? null : resolveExitInput({
49720
+ ledger: workspace.ledger,
49721
+ exit: input.way,
49722
+ where: "way",
49723
+ failures
49724
+ });
49725
+ if (failures.length > 0) {
49726
+ throw new CliError(`Nothing was saved — some links do not resolve to pushed files:
49727
+ ` + failures.map((failure2) => ` ${failure2.where}: "${failure2.link}"` + `${failure2.reason === "unpushed" ? " (tracked but not pushed yet)" : " (no tracked file matches)"}`).join(`
49728
+ `), 2);
49729
+ }
49730
+ const parsedWay = way === null ? null : PairWaySchema.safeParse(way);
49731
+ const parsed = PlacePairSchema.safeParse({
49732
+ kind: input.relation,
49733
+ ...parentRef ? { parentRef } : {},
49734
+ way: parsedWay?.success ? parsedWay.data : way,
49735
+ words: typeof input.words === "string" ? { text: input.words, first: input.wordsFirst === "other" ? "b" : "a" } : null
49736
+ });
49737
+ if (!parsed.success) {
49738
+ throw new CliError(`The link JSON failed validation:
49739
+ ` + parsed.error.issues.map((issue3) => {
49740
+ const field = issue3.path.join(".").replace(PAIR_KIND_FIELD_RE, "relation");
49741
+ return ` ${field || "(root)"}: ${issue3.message}`;
49742
+ }).join(`
49743
+ `), 2);
49744
+ }
49745
+ const problem = placePairProblem({ pair: parsed.data, aRef, bRef });
49746
+ if (problem) {
49747
+ throw new CliError(problem, 2);
49748
+ }
49749
+ return parsed.data;
49750
+ }
49751
+ async function runLink(argv, mode) {
49752
+ const args = parseCommandArgs({
49753
+ argv,
49754
+ flags: { file: { type: "string" }, json: { type: "boolean" } },
49755
+ maxPositionals: 2
49756
+ });
49757
+ const [path, otherPath] = args.positionals;
49758
+ const inputPath = stringFlag(args, "file");
49759
+ if (!(path && otherPath) || mode === "link" !== Boolean(inputPath)) {
49760
+ throw new CliError(META_USAGE, 2);
49761
+ }
49762
+ const workspace = await loadWorkspaceContext();
49763
+ const entry = resolveTrackedEntry({ ledger: workspace.ledger, input: path });
49764
+ const other = resolveTrackedEntry({
49765
+ ledger: workspace.ledger,
49766
+ input: otherPath
49767
+ });
49768
+ const pair = inputPath ? translatePlacePairInput({
49769
+ workspace,
49770
+ raw: await readJsonFile2(inputPath),
49771
+ aRef: entry.referenceId,
49772
+ bRef: other.referenceId
49773
+ }) : null;
49774
+ const client = await workspaceClient(workspace);
49775
+ const { changedFileIds } = await client.putPlacePair({
49776
+ fileId: entry.fileId,
49777
+ otherFileId: other.fileId,
49778
+ pair
49779
+ });
49780
+ for (const fileId of changedFileIds) {
49781
+ const meta3 = await client.fetchFileMeta({ fileId });
49782
+ await freshenLedgerAfterMetaWrite({
49783
+ root: workspace.root,
49784
+ ledger: workspace.ledger,
49785
+ fileId,
49786
+ versionId: meta3.currentVersion.id
49787
+ });
49788
+ }
49789
+ if (args.booleans.has("json")) {
49790
+ printJson({ path, other: otherPath, saved: true, pair, changedFileIds });
49791
+ return 0;
49792
+ }
49793
+ const changed = changedFileIds.length === 0 ? "nothing changed" : `${changedFileIds.length} location${changedFileIds.length === 1 ? "" : "s"} changed`;
49794
+ printLines([
49795
+ mode === "link" ? `Linked ${path} and ${otherPath} (${pair?.kind}): ${changed}.` : `Unlinked ${path} and ${otherPath}: ${changed}.`,
49796
+ "See where each location now sits with `craft meta get`."
49797
+ ]);
49798
+ return 0;
49799
+ }
49223
49800
 
49224
49801
  // src/commands/mv.ts
49225
49802
  import { existsSync as existsSync8 } from "node:fs";
@@ -56055,9 +56632,12 @@ var HELP_COMMAND_LINES = ` clone <projectIdOrUrl> [dir] Download a project as
56055
56632
  map get|set <location-path> Read/author the interactive map stored on a
56056
56633
  location file (see "craft help map")
56057
56634
  meta get <path> Show a file's designation metadata (game start
56058
- config, GM triggers, playable flag, map summary)
56635
+ config, GM triggers, playable flag, map summary,
56636
+ a location's transport and linked locations)
56059
56637
  meta set-game-start|set-gm-trigger|set-playable
56060
56638
  Configure playability metadata (see "craft help meta")
56639
+ meta set-transport|link|unlink Make a location a ship/caravan, or link two
56640
+ locations (see "craft help meta")
56061
56641
  vn list|create|pull|check|push|publish|assets
56062
56642
  Write visual novels: pull a story to a JSON
56063
56643
  file, edit it, push it back, publish it
@@ -56389,6 +56969,10 @@ Authoring format = the stored MapData shape with conveniences:
56389
56969
  - element "id"s are minted when omitted; point style defaults are applied.
56390
56970
  - required: "aspectRatio" matching the background image (4:3 for a default
56391
56971
  landscape-4-3 generation). background/scale/grid default to null.
56972
+ - an area/point may carry "exit": a way to another location, e.g.
56973
+ {"kind":"stairs","to":"Places/The Inn Upstairs.location.json","direction":"up"}.
56974
+ "to" takes a workspace path (or slug.type / referenceId); leave it out for
56975
+ the way out to the parent map. \`get\` prints links and exit "to"s as paths.
56392
56976
 
56393
56977
  The background image URL must be Craft-hosted \u2014 generate one with
56394
56978
  \`craft image generate\` (or upload with \`craft image upload\`) and paste
@@ -56440,6 +57024,9 @@ chapters, where cutouts stand (desktop and phone), and pictures on pages.
56440
57024
  craft meta set-game-start <game-start-path> --file <json>
56441
57025
  craft meta set-gm-trigger <gm-instruction-path> (--file <json> | --clear)
56442
57026
  craft meta set-playable <character-path> <true|false>
57027
+ craft meta set-transport <location-path> (--file <json> | --clear)
57028
+ craft meta link <location-path> <other-location-path> --file <json>
57029
+ craft meta unlink <location-path> <other-location-path>
56443
57030
 
56444
57031
  Designation metadata is app-managed and lives outside the working tree;
56445
57032
  these commands write the server directly (the target file must be pushed).
@@ -56461,6 +57048,23 @@ set-gm-trigger JSON (triggered GM rules; name \u2264120 / body \u2264500 chars):
56461
57048
 
56462
57049
  Read .craft/instructions/making-it-playable.md for how these fit together.
56463
57050
 
57051
+ set-transport JSON (a location that carries people; its deck plan is its map):
57052
+ {"kind":"ship","travels":"water","speed":{"value":6,"unit":"knots"},"capacity":40}
57053
+ kind: ship|boat|airship|wagon|caravan|carriage|train|car|bus|plane|mount|other
57054
+
57055
+ link JSON \u2014 how <location> relates to <other>, the shape \`meta get\` prints
57056
+ under linkedLocations (it replaces whatever lay between the two):
57057
+ {"relation":"part_of","way":{"kind":"stairs","direction":"up"}}
57058
+ relation: same_place (both parts of "in": "<path>") | contains (other is part
57059
+ of location) | part_of (location is part of other) | separate (needs "way") |
57060
+ other (needs "words": "is the Shadowfell reflection of", "wordsFirst":
57061
+ "location"|"other"). way: {kind, direction from location, oneWay:
57062
+ "there"|"back", locked, hidden, when, requires, label, distance, time}.
57063
+ A location is inside at most one place and never inside itself; linking can
57064
+ move a place in or out of another. unlink removes everything between them.
57065
+ Read .craft/instructions/linking-locations.md for exits, transports and
57066
+ linked locations.
57067
+
56464
57068
  upload: send a local image file (max 10MB; 50 per 24h, 1,000 per 24h for
56465
57069
  subscribers) to the project's media
56466
57070
  storage; the server re-encodes it to webp and returns the same pasteable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftrpgs/cli",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-alpha.1",
4
4
  "description": "Sync Craft projects with a local folder — clone, edit with any coding agent, push back.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://craftrpgs.com",