Skip to content

Commit 111fd7e

Browse files
committed
feat: support --world and --position for server logs
1 parent ee210ee commit 111fd7e

4 files changed

Lines changed: 112 additions & 43 deletions

File tree

packages/@dcl/sdk-commands/src/commands/sdk-server-logs/index.ts

Lines changed: 107 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Router } from '@well-known-components/http-server'
99

1010
import { declareArgs } from '../../logic/args'
1111
import { CliComponents } from '../../components'
12+
import { CliError } from '../../logic/error'
1213
import { printError } from '../../logic/beautiful-logs'
1314
import { createWallet } from '../../logic/account'
1415
import { 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'
4044
export 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+
258318
export 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(/\.dcl\.eth$/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(/\.dcl\.eth$/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) => {

packages/@dcl/sdk-commands/src/logic/error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export type CliErrorName =
7575
| 'STORAGE_MISSING_WORLD'
7676
// Server logs errors
7777
| 'SERVER_LOGS_MISSING_WORLD'
78-
| 'SERVER_LOGS_MISSING_MULTIPLAYER_ID'
78+
| 'SERVER_LOGS_INVALID_POSITION'
7979

8080
export class CliError<T extends CliErrorName> extends Error {
8181
constructor(public name: T = 'CliError' as T, public message: string = '', public stack?: string) {

packages/@dcl/sdk-commands/src/logic/scene-validations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { getPublishableFiles } from './project-files'
1111
import { printWarning } from './beautiful-logs'
1212

1313
/**
14-
* Extended Scene type that includes authoritativeMultiplayer flag
15-
* for enabling Authoritative Server integration.
14+
* Extended Scene type that includes the authoritativeMultiplayer flag for
15+
* enabling Authoritative Server integration.
1616
*/
1717
export type SceneWithMultiplayer = Scene & {
1818
authoritativeMultiplayer?: boolean

test/build-ecs/fixtures/dcl-test-lib-integration/tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"include": ["src"],
33
"compilerOptions": {
4+
"ignoreDeprecations": "6.0",
45
"module": "ESNext",
6+
"rootDir": "src",
57
"outDir": "bin",
68
"declaration": true
79
},

0 commit comments

Comments
 (0)