@@ -9,6 +9,7 @@ import { Router } from '@well-known-components/http-server'
99
1010import { declareArgs } from '../../logic/args'
1111import { CliComponents } from '../../components'
12+ import { CliError } from '../../logic/error'
1213import { printError } from '../../logic/beautiful-logs'
1314import { createWallet } from '../../logic/account'
1415import { createAuthChainHeaders } from '../../logic/auth-chain-headers'
@@ -26,6 +27,9 @@ export const args = declareArgs({
2627 '--help' : Boolean ,
2728 '-h' : '--help' ,
2829 '--dir' : String ,
30+ '--world' : String ,
31+ '-w' : '--world' ,
32+ '--position' : String ,
2933 '--target' : String ,
3034 '-t' : '--target' ,
3135 '--port' : Number ,
@@ -40,28 +44,39 @@ const DEFAULT_SERVER = 'https://multiplayer-server.decentraland.org'
4044export function help ( options : Options ) {
4145 options . components . logger . log ( `
4246 Usage: 'sdk-commands sdk-server-logs [options]'
43- Streams real-time logs from the multiplayer server for your scene.
44- The scene identifier is automatically determined from scene.json:
45- - Worlds: uses worldConfiguration.name
46- - Genesis city scenes: uses scene.base parcel
47+ Streams real-time logs from the multiplayer server.
48+ Runs from any directory when --world and/or --position are provided.
49+ Falls back to scene.json (worldConfiguration.name) when neither is passed.
4750
4851 Options:
49- -h, --help Displays complete help
50- -t, --target [URL] Target multiplayer server URL (default: ${ DEFAULT_SERVER } )
51- --dir [path] Path to the project directory
52- -p, --port [port] Select a custom port for the linker dApp
53- -b, --no-browser Do not open a new browser window
54- --https Use HTTPS for the linker dApp
52+ -h, --help Displays complete help
53+ -w, --world [name] World name. When omitted, --position targets Genesis City.
54+ --position [x,y] Parcel coordinates. Optional for worlds (selects a scene
55+ in a multi-scene world). Required for Genesis City.
56+ -t, --target [URL] Target multiplayer server URL (default: ${ DEFAULT_SERVER } )
57+ --dir [path] Path to the project directory (used with scene.json fallback)
58+ -p, --port [port] Select a custom port for the linker dApp
59+ -b, --no-browser Do not open a new browser window
60+ --https Use HTTPS for the linker dApp
5561
5662 Examples:
57- - View logs for your scene (run from project directory) :
63+ - View logs using the world in the current scene.json :
5864 $ sdk-commands sdk-server-logs
5965
60- - Connect to local development server:
61- $ sdk-commands sdk-server-logs --target http://localhost:8000
66+ - View logs for a world from any directory:
67+ $ sdk-commands sdk-server-logs --world myworld.dcl.eth
68+
69+ - View logs for a specific scene in a multi-scene world:
70+ $ sdk-commands sdk-server-logs --world myworld.dcl.eth --position 10,10
71+
72+ - View logs for a Genesis City scene (use =value form for negative coords):
73+ $ sdk-commands sdk-server-logs --position=-125,-96
74+
75+ - Connect to a custom server:
76+ $ sdk-commands sdk-server-logs --world myworld --target https://multiplayer-server.decentraland.zone
6277
6378 - Use private key for authentication (no browser):
64- $ DCL_PRIVATE_KEY=0x... sdk-commands sdk-server-logs
79+ $ DCL_PRIVATE_KEY=0x... sdk-commands sdk-server-logs --world myworld
6580` )
6681}
6782
@@ -103,7 +118,7 @@ async function getAddressAndSignature(
103118 components : CliComponents ,
104119 awaitResponse : IFuture < void > ,
105120 payload : string ,
106- sceneIdentifier : string ,
121+ displayLabel : string ,
107122 targetUrl : string ,
108123 linkOptions : Omit < dAppOptions , 'uri' > ,
109124 signCallback : ( response : LinkerResponse ) => Promise < void >
@@ -130,7 +145,7 @@ async function getAddressAndSignature(
130145 skipValidations : true ,
131146 debug : ! ! process . env . DEBUG ,
132147 isWorld : true ,
133- world : sceneIdentifier ,
148+ world : displayLabel ,
134149 targetUrl,
135150 action : 'view-logs'
136151 } )
@@ -255,29 +270,88 @@ function formatAndPrintLog(logger: CliComponents['logger'], log: any) {
255270 }
256271}
257272
273+ function normalizePosition ( raw : string ) : string {
274+ const match = raw . trim ( ) . match ( / ^ ( - ? \d + ) \s * , \s * ( - ? \d + ) $ / )
275+ if ( ! match ) {
276+ throw new CliError (
277+ 'SERVER_LOGS_INVALID_POSITION' ,
278+ `Invalid --position "${ raw } "; expected "x,y" with integer coordinates`
279+ )
280+ }
281+ return `${ parseInt ( match [ 1 ] , 10 ) } ,${ parseInt ( match [ 2 ] , 10 ) } `
282+ }
283+
284+ interface LogsRequest {
285+ logsUrl : string
286+ pathname : string
287+ metadata : string
288+ }
289+
290+ /**
291+ * Build the HTTP request shape for a given target.
292+ *
293+ * Three shapes map to three CLI use cases:
294+ * - world only → `/logs/:world` (single-scene)
295+ * - world + position → `/logs/:world?position=x,y` (multi-scene world)
296+ * - position only (no world) → `/logs` with parcel in metadata (Genesis City)
297+ *
298+ * Query strings are not part of the signed payload, so `pathname` is always the
299+ * bare route. Genesis City is the only shape that ships data in metadata.
300+ */
301+ function buildLogsRequest ( baseURL : string , world : string | undefined , position : string | undefined ) : LogsRequest {
302+ if ( world ) {
303+ const query = position ? `?position=${ encodeURIComponent ( position ) } ` : ''
304+ return {
305+ logsUrl : `${ baseURL } /logs/${ world } ${ query } ` ,
306+ pathname : `/logs/${ world } ` ,
307+ metadata : JSON . stringify ( { } )
308+ }
309+ }
310+
311+ return {
312+ logsUrl : `${ baseURL } /logs` ,
313+ pathname : '/logs' ,
314+ metadata : JSON . stringify ( { parcel : position } )
315+ }
316+ }
317+
258318export async function main ( options : Options ) {
259319 const { logger } = options . components
260320 const projectRoot = resolve ( process . cwd ( ) , options . args [ '--dir' ] || '.' )
261321
262- // Validate workspace exists
263- await getValidWorkspace ( options . components , projectRoot )
264-
265- const sceneJson = await getValidSceneJson ( options . components , projectRoot )
266- const worldName = sceneJson . worldConfiguration ?. name
267- const isWorld = ! ! worldName
268- const sceneIdentifier = isWorld ? worldName : sceneJson . scene . base
269-
270- // Determine target URL
271- const baseURL = options . args [ '--target' ] || DEFAULT_SERVER
322+ const positionArg = options . args [ '--position' ]
323+ const worldArg = options . args [ '--world' ]
324+
325+ const position = positionArg ? normalizePosition ( positionArg ) : undefined
326+ let world : string | undefined
327+
328+ if ( worldArg ) {
329+ world = worldArg . replace ( / \. d c l \. e t h $ / i, '' )
330+ } else if ( ! position ) {
331+ // no --world nor --position: fall back to scene.json in the current directory
332+ await getValidWorkspace ( options . components , projectRoot )
333+ const sceneJson = await getValidSceneJson ( options . components , projectRoot )
334+ const worldName = sceneJson . worldConfiguration ?. name
335+ if ( ! worldName ) {
336+ throw new CliError (
337+ 'SERVER_LOGS_MISSING_WORLD' ,
338+ 'Provide --world (with optional --position) for worlds, or --position alone for Genesis City scenes. ' +
339+ 'Alternatively, run from a project whose scene.json defines worldConfiguration.name.'
340+ )
341+ }
342+ world = worldName . replace ( / \. d c l \. e t h $ / i, '' )
343+ }
272344
273- // Build the logs URL
274- const logsUrl = `${ baseURL } /logs`
345+ const displayLabel = world
346+ ? position
347+ ? `${ world } .dcl.eth ${ position } `
348+ : `${ world } .dcl.eth`
349+ : `Genesis City ${ position } `
275350
276- logger . info ( `Viewing logs for ${ isWorld ? 'world' : 'scene' } : ${ sceneIdentifier } ` )
277- logger . info ( `Target: ${ logsUrl } ` )
351+ logger . info ( `Viewing logs for ${ displayLabel } ` )
278352
279- // Build the pathname for signing
280- const pathname = '/logs'
353+ const baseURL = options . args [ '--target' ] || DEFAULT_SERVER
354+ const { logsUrl , pathname, metadata } = buildLogsRequest ( baseURL , world , position )
281355
282356 // Linker dApp options
283357 const linkerPort = options . args [ '--port' ]
@@ -287,13 +361,6 @@ export async function main(options: Options) {
287361
288362 const awaitResponse = future < void > ( )
289363 const timestamp = String ( Date . now ( ) )
290- // Build metadata following the standard signedFetch format
291- const metadata = JSON . stringify ( {
292- parcel : sceneJson . scene . base ,
293- realm : { serverName : isWorld ? worldName : 'main' } ,
294- realmName : isWorld ? worldName : 'main' ,
295- sceneId : isWorld ? worldName : undefined
296- } )
297364
298365 // Build the payload to sign: method:path:timestamp:metadata
299366 const payload = [ 'get' , pathname , timestamp , metadata ] . join ( ':' ) . toLowerCase ( )
@@ -304,7 +371,7 @@ export async function main(options: Options) {
304371 options . components ,
305372 awaitResponse ,
306373 payload ,
307- sceneIdentifier ,
374+ displayLabel ,
308375 baseURL ,
309376 linkOptions ,
310377 async ( linkerResponse ) => {
0 commit comments