@@ -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,86 @@ 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 the multiplayer-server `/logs` endpoint.
292+ *
293+ * Every call targets the same `/logs` route; the server picks the scene from
294+ * the signed metadata:
295+ * - worlds → `sceneId: "<world>.dcl.eth"` (+ `parcel` for multi-scene worlds)
296+ * - Genesis → `parcel: "x,y"` only
297+ *
298+ * `parcel` and `realmName` are always set. `realmName` defaults to `"main"` for
299+ * Genesis, matching the convention used by other signed-fetch emitters
300+ * (hammurabi worker, unity explorer, kernel).
301+ */
302+ function buildLogsRequest ( baseURL : string , world : string | undefined , position : string | undefined ) : LogsRequest {
303+ const metadata = JSON . stringify ( {
304+ parcel : position ?? '0,0' ,
305+ realmName : world ? `${ world } .dcl.eth` : 'main' ,
306+ ...( world && { sceneId : `${ world } .dcl.eth` } )
307+ } )
308+
309+ return {
310+ logsUrl : `${ baseURL } /logs` ,
311+ pathname : '/logs' ,
312+ metadata
313+ }
314+ }
315+
258316export async function main ( options : Options ) {
259317 const { logger } = options . components
260318 const projectRoot = resolve ( process . cwd ( ) , options . args [ '--dir' ] || '.' )
261319
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
320+ const positionArg = options . args [ '--position' ]
321+ const worldArg = options . args [ '--world' ]
322+
323+ const position = positionArg ? normalizePosition ( positionArg ) : undefined
324+ let world : string | undefined
325+
326+ if ( worldArg ) {
327+ world = worldArg . replace ( / \. d c l \. e t h $ / i, '' )
328+ } else if ( ! position ) {
329+ // no --world nor --position: fall back to scene.json in the current directory
330+ await getValidWorkspace ( options . components , projectRoot )
331+ const sceneJson = await getValidSceneJson ( options . components , projectRoot )
332+ const worldName = sceneJson . worldConfiguration ?. name
333+ if ( ! worldName ) {
334+ throw new CliError (
335+ 'SERVER_LOGS_MISSING_WORLD' ,
336+ 'Provide --world (with optional --position) for worlds, or --position alone for Genesis City scenes. ' +
337+ 'Alternatively, run from a project whose scene.json defines worldConfiguration.name.'
338+ )
339+ }
340+ world = worldName . replace ( / \. d c l \. e t h $ / i, '' )
341+ }
272342
273- // Build the logs URL
274- const logsUrl = `${ baseURL } /logs`
343+ const displayLabel = world
344+ ? position
345+ ? `${ world } .dcl.eth ${ position } `
346+ : `${ world } .dcl.eth`
347+ : `Genesis City ${ position } `
275348
276- logger . info ( `Viewing logs for ${ isWorld ? 'world' : 'scene' } : ${ sceneIdentifier } ` )
277- logger . info ( `Target: ${ logsUrl } ` )
349+ logger . info ( `Viewing logs for ${ displayLabel } ` )
278350
279- // Build the pathname for signing
280- const pathname = '/logs'
351+ const baseURL = options . args [ '--target' ] || DEFAULT_SERVER
352+ const { logsUrl , pathname, metadata } = buildLogsRequest ( baseURL , world , position )
281353
282354 // Linker dApp options
283355 const linkerPort = options . args [ '--port' ]
@@ -287,13 +359,6 @@ export async function main(options: Options) {
287359
288360 const awaitResponse = future < void > ( )
289361 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- } )
297362
298363 // Build the payload to sign: method:path:timestamp:metadata
299364 const payload = [ 'get' , pathname , timestamp , metadata ] . join ( ':' ) . toLowerCase ( )
@@ -304,7 +369,7 @@ export async function main(options: Options) {
304369 options . components ,
305370 awaitResponse ,
306371 payload ,
307- sceneIdentifier ,
372+ displayLabel ,
308373 baseURL ,
309374 linkOptions ,
310375 async ( linkerResponse ) => {
0 commit comments