From 52ca229352d9f4c4fc3ab3fdd1c5fb775dfc2cab Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 19 Sep 2025 22:30:25 +0200 Subject: [PATCH 1/8] chore: add libc compatiblity for dotnet (dont ask) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 4b3d7d5..5e8c313 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ FROM node:20-alpine AS base ARG app_dir WORKDIR ${app_dir} +RUN apk update && apk add libc6-compat # * Installing production dependencies FROM base AS dependencies From 25b871e3b5949c9e8df23a98c1bd351d5b4df245 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 19 Sep 2025 22:30:45 +0200 Subject: [PATCH 2/8] feat: add JSON output support to CLI file commands --- src/cli/cli.ts | 1 + src/cli/files.cmd.ts | 50 +++++++++++++++++++++++++++----------------- src/cli/output.ts | 29 +++++++++++++++++++++++++ src/cli/utils.ts | 32 ++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 src/cli/output.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 03c0e11..b2a3ec9 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -7,6 +7,7 @@ import { importCmd } from './import.cmd'; const program = baseProgram .name('BOSS') .description('CLI to manage and view BOSS data') + .option('--json', 'Output as JSON') .addCommand(appCmd) .addCommand(taskCmd) .addCommand(importCmd) diff --git a/src/cli/files.cmd.ts b/src/cli/files.cmd.ts index 51ac755..d59174a 100644 --- a/src/cli/files.cmd.ts +++ b/src/cli/files.cmd.ts @@ -4,32 +4,39 @@ import { Readable } from 'node:stream'; import { request } from 'undici'; import { Command } from 'commander'; import { decryptWiiU } from '@pretendonetwork/boss-crypto'; -import { getCliContext } from './utils'; +import { commandHandler, getCliContext } from './utils'; +import { logOutputList, logOutputObject } from './output'; const listCmd = new Command('ls') .description('List all task files in BOSS') .argument('', 'BOSS app to search in') .argument('', 'Task to search in') - .action(async (appId: string, taskId: string) => { + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; const ctx = getCliContext(); const { files } = await ctx.grpc.listFiles({ bossAppId: appId, taskId: taskId }); - console.table(files.map(v => ({ - 'Data ID': Number(v.dataId), - 'Name': v.name, - 'Type': v.type, - 'Size (bytes)': Number(v.size) - }))); - }); + logOutputList(cmd.format, files.map(v => ({ + ...v, + size: Number(v.size), + dataId: Number(v.dataId) + }), { + dataId: 'Data ID', + name: 'Name', + type: 'Type', + size: 'Size (bytes)' + })); + })); const viewCmd = new Command('view') .description('Look up a specific task file') .argument('', 'BOSS app that contains the task') .argument('', 'Task that contains the task file') .argument('', 'Task file ID to lookup', BigInt) - .action(async (appId: string, taskId: string, dataId: bigint) => { + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + const [appId, taskId, dataId] = cmd.args; const ctx = getCliContext(); const { files } = await ctx.grpc.listFiles({ bossAppId: appId, @@ -40,7 +47,7 @@ const viewCmd = new Command('view') console.log(`Could not find task file with data ID ${dataId} in task ${taskId}`); return; } - console.log({ + logOutputObject(cmd.format, { dataId: Number(file.dataId), name: file.name, type: file.type, @@ -56,7 +63,7 @@ const viewCmd = new Command('view') createdAt: new Date(Number(file.createdTimestamp)), updatedAt: new Date(Number(file.updatedTimestamp)) }); - }); + })); const downloadCmd = new Command('download') .description('Download a task file') @@ -64,7 +71,8 @@ const downloadCmd = new Command('download') .argument('', 'Task that contains the task file') .argument('', 'Task file ID to lookup', BigInt) .option('-d, --decrypt', 'Decrypt the file before return') - .action(async (appId: string, taskId: string, dataId: bigint, ops: { decrypt: boolean }) => { + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + const [appId, taskId, dataId] = cmd.args; const ctx = getCliContext(); const { files } = await ctx.grpc.listFiles({ bossAppId: appId, @@ -94,14 +102,14 @@ const downloadCmd = new Command('download') let buffer: Buffer = Buffer.concat(chunks); - if (ops.decrypt) { + if (cmd.opts().decrypt) { const keys = ctx.getWiiUKeys(); const decrypted = decryptWiiU(buffer, keys.aesKey, keys.hmacKey); buffer = decrypted.content; } await pipeline(Readable.from(buffer), process.stdout); - }); + })); const createCmd = new Command('create') .description('Create a new task file') @@ -115,7 +123,9 @@ const createCmd = new Command('create') .option('--name-as-id', 'Force the name as the data ID') .option('--notify-new ', 'Add entry to NotifyNew') .option('--notify-led', 'Enable NotifyLED') - .action(async (appId: string, taskId: string, opts: { name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; nameAsId?: boolean; type: string; file: string }) => { + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; + const opts = cmd.opts<{ name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; nameAsId?: boolean; type: string; file: string }>(); const fileBuf = await fs.readFile(opts.file); const ctx = getCliContext(); const { file } = await ctx.grpc.uploadFile({ @@ -135,21 +145,23 @@ const createCmd = new Command('create') return; } console.log(`Created file with ID ${file.dataId}`); - }); + })); const deleteCmd = new Command('delete') .description('Delete a task file') .argument('', 'BOSS app that contains the task') .argument('', 'Task that contains the task file') .argument('', 'Task file ID to delete', BigInt) - .action(async (appId: string, taskId: string, dataId: bigint) => { + .action(commandHandler<[string, string, bigint]>(async (cmd): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- I want to use destructuring + const [appId, _taskId, dataId] = cmd.args; const ctx = getCliContext(); await ctx.grpc.deleteFile({ bossAppId: appId, dataId: dataId }); console.log(`Deleted task file with ID ${dataId}`); - }); + })); export const fileCmd = new Command('file') .description('Manage all the task files in BOSS') diff --git a/src/cli/output.ts b/src/cli/output.ts new file mode 100644 index 0000000..56ee346 --- /dev/null +++ b/src/cli/output.ts @@ -0,0 +1,29 @@ +export type FormattableObject = Record; +export type FieldMapping = Record; +export type FormatOption = 'json' | 'pretty'; + +function mapOutputObject(obj: FormattableObject, mapping: FieldMapping): FormattableObject { + const entries = Object.entries(obj); + const mappedEntries = entries.map((v) => { + const newKey = mapping[v[0]] ?? v[0]; + return [newKey, v[1]] as const; + }); + return Object.fromEntries(mappedEntries); +} + +export function logOutputList(format: FormatOption, items: T[], mapping: FieldMapping = {}): void { + if (format === 'json') { + console.log(JSON.stringify(items, null, 2)); + return; + } + const mappedItems = items.map(item => mapOutputObject(item, mapping)); + console.table(mappedItems); +} + +export function logOutputObject(format: FormatOption, obj: T, mapping: FieldMapping = {}): void { + if (format === 'json') { + console.log(JSON.stringify(obj, null, 2)); + return; + } + console.log(mapOutputObject(obj, mapping)); +} diff --git a/src/cli/utils.ts b/src/cli/utils.ts index c82a3ee..1bca5a1 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -2,6 +2,8 @@ import { BOSSDefinition } from '@pretendonetwork/grpc/boss/boss_service'; import { createChannel, createClient, Metadata } from 'nice-grpc'; import dotenv from 'dotenv'; import type { BOSSClient } from '@pretendonetwork/grpc/boss/boss_service'; +import type { Command } from 'commander'; +import type { FormatOption } from './output'; export type WiiUKeys = { aesKey: string; hmacKey: string }; export type NpdiUrl = { @@ -71,3 +73,33 @@ export function prettyTrunc(str: string, len: number): string { } return str; } + +export type CommandHandlerCtx = { + opts: >() => T; + globalOpts: { + json?: boolean; + }; + format: FormatOption; + args: T; +}; + +export function commandHandler(cb: (ctx: CommandHandlerCtx) => Promise) { + return (...args: any[]): Promise => { + const cmd: Command = args[args.length - 1]; + + let topCmd = cmd; + while (topCmd.parent) { + topCmd = topCmd.parent; + } + const globalOpts = topCmd.opts() ?? {}; + + const ctx: CommandHandlerCtx = { + args: args as T, + globalOpts, + format: globalOpts.json ? 'json' : 'pretty', + opts: () => cmd.opts() as any + }; + console.log(globalOpts); + return cb(ctx); + }; +} From d8b2a8dcba106738a22151d997a82de907cb5a32 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Fri, 19 Sep 2025 21:44:32 +0100 Subject: [PATCH 3/8] fix: handle bigints in json cli serialisation --- src/cli/output.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/output.ts b/src/cli/output.ts index 56ee346..7970cec 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -13,7 +13,12 @@ function mapOutputObject(obj: FormattableObject, mapping: FieldMapping): Formatt export function logOutputList(format: FormatOption, items: T[], mapping: FieldMapping = {}): void { if (format === 'json') { - console.log(JSON.stringify(items, null, 2)); + console.log(JSON.stringify(items, (_, v) => { + if (typeof v === 'bigint') { + return v.toString(); + } + return v; + }, 2)); return; } const mappedItems = items.map(item => mapOutputObject(item, mapping)); From 6b95420e0fccd3e6ee7ba74550e27ddc52218c2e Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 19 Sep 2025 22:47:18 +0200 Subject: [PATCH 4/8] feat: add json logging to rest of CLI --- src/cli/apps.cmd.ts | 28 ++++++++++++++++------------ src/cli/tasks.cmd.ts | 36 +++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/cli/apps.cmd.ts b/src/cli/apps.cmd.ts index 62a0289..59f3b16 100644 --- a/src/cli/apps.cmd.ts +++ b/src/cli/apps.cmd.ts @@ -1,23 +1,27 @@ import { Command } from 'commander'; -import { getCliContext, prettyTrunc } from './utils'; +import { commandHandler, getCliContext, prettyTrunc } from './utils'; +import { logOutputList, logOutputObject } from './output'; const listCmd = new Command('ls') .description('List all apps in BOSS') - .action(async () => { + .action(commandHandler<[]>(async (cmd): Promise => { const ctx = getCliContext(); const { apps } = await ctx.grpc.listKnownBOSSApps({}); - console.table(apps.map(v => ({ - 'App ID': v.bossAppId, - 'Name': prettyTrunc(v.name, 20), - 'Title ID': v.titleId, - 'Title region': v.titleRegion - }))); - }); + logOutputList(cmd.format, apps.map(v => ({ + name: prettyTrunc(v.name, 20) + })), { + bossAppId: 'App ID', + name: 'Name', + titleId: 'Title ID', + titleRegion: 'Title region' + }); + })); const viewCmd = new Command('view') .description('Look up a specific BOSS app') .argument('', 'BOSS app ID to lookup') - .action(async (id: string) => { + .action(commandHandler<[string]>(async (cmd): Promise => { + const [id] = cmd.args; const ctx = getCliContext(); const { apps } = await ctx.grpc.listKnownBOSSApps({}); const app = apps.find(v => v.bossAppId === id); @@ -26,14 +30,14 @@ const viewCmd = new Command('view') return; } - console.log({ + logOutputObject(cmd.format, { appId: app.bossAppId, name: app.name, titleId: app.titleId, titleRegion: app.titleRegion, knownTasks: app.tasks }); - }); + })); export const appCmd = new Command('app') .description('Manage all the apps in BOSS') diff --git a/src/cli/tasks.cmd.ts b/src/cli/tasks.cmd.ts index bc70229..432344d 100644 --- a/src/cli/tasks.cmd.ts +++ b/src/cli/tasks.cmd.ts @@ -1,25 +1,28 @@ import { Command } from 'commander'; -import { getCliContext } from './utils'; +import { commandHandler, getCliContext } from './utils'; +import { logOutputList, logOutputObject } from './output'; const listCmd = new Command('ls') .description('List all tasks in BOSS') .argument('', 'BOSS app to search in') - .action(async (appId: string) => { + .action(commandHandler<[string]>(async (cmd): Promise => { + const [appId] = cmd.args; const ctx = getCliContext(); const { tasks } = await ctx.grpc.listTasks({}); const filteredTasks = tasks.filter(v => v.bossAppId === appId); - console.table(filteredTasks.map(v => ({ - 'Task ID': v.id, - 'Description': v.description, - 'Status': v.status - }))); - }); + logOutputList(cmd.format, filteredTasks, { + id: 'Task ID', + description: 'Description', + status: 'Status' + }); + })); const viewCmd = new Command('view') .description('Look up a specific task') .argument('', 'BOSS app ID that contains the task') .argument('', 'Task ID to lookup') - .action(async (appId: string, taskId: string) => { + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; const ctx = getCliContext(); const { tasks } = await ctx.grpc.listTasks({}); const task = tasks.find(v => v.bossAppId === appId && v.id === taskId); @@ -27,7 +30,7 @@ const viewCmd = new Command('view') console.log(`Could not find task with ID ${taskId} in app ${appId}`); return; } - console.log({ + logOutputObject(cmd.format, { taskId: task.id, inGameId: task.inGameId, description: task.description, @@ -38,7 +41,7 @@ const viewCmd = new Command('view') createdAt: new Date(Number(task.createdTimestamp)), updatedAt: new Date(Number(task.updatedTimestamp)) }); - }); + })); const createCmd = new Command('create') .description('Create a new task') @@ -46,8 +49,10 @@ const createCmd = new Command('create') .requiredOption('--id ', 'Id of the task') .requiredOption('--title-id ', 'Title ID for the task') .option('--desc [desc]', 'Description of the task') - .action(async (appId: string, opts: { id: string; titleId: string; desc?: string }) => { + .action(commandHandler<[string]>(async (cmd): Promise => { + const [appId] = cmd.args; const ctx = getCliContext(); + const opts = cmd.opts<{ id: string; titleId: string; desc?: string }>(); const { task } = await ctx.grpc.registerTask({ bossAppId: appId, id: opts.id, @@ -60,20 +65,21 @@ const createCmd = new Command('create') return; } console.log(`Created task with ID ${task.id}`); - }); + })); const deleteCmd = new Command('delete') .description('Delete a task') .argument('', 'BOSS app ID that contains the task') .argument('', 'Task ID to delete') - .action(async (appId: string, taskId: string) => { + .action(commandHandler<[string, string]>(async (cmd): Promise => { + const [appId, taskId] = cmd.args; const ctx = getCliContext(); await ctx.grpc.deleteTask({ bossAppId: appId, id: taskId }); console.log(`Deleted task with ID ${taskId}`); - }); + })); export const taskCmd = new Command('task') .description('Manage all the tasks in BOSS') From 40a030808830c30402e0a376d64697b9c9ef45e5 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Fri, 19 Sep 2025 21:54:50 +0100 Subject: [PATCH 5/8] fix: remove rouge console log --- src/cli/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 1bca5a1..6d7ab63 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -99,7 +99,7 @@ export function commandHandler(cb: (ctx: CommandHandlerCtx) format: globalOpts.json ? 'json' : 'pretty', opts: () => cmd.opts() as any }; - console.log(globalOpts); + return cb(ctx); }; } From f8fca8f082e7553584480811336a025c21dfa060 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Fri, 19 Sep 2025 22:53:50 +0100 Subject: [PATCH 6/8] fix: use json replacer for both util functions --- src/cli/files.cmd.ts | 8 ++++---- src/cli/output.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/cli/files.cmd.ts b/src/cli/files.cmd.ts index d59174a..c995130 100644 --- a/src/cli/files.cmd.ts +++ b/src/cli/files.cmd.ts @@ -20,8 +20,8 @@ const listCmd = new Command('ls') }); logOutputList(cmd.format, files.map(v => ({ ...v, - size: Number(v.size), - dataId: Number(v.dataId) + size: v.size, + dataId: v.dataId }), { dataId: 'Data ID', name: 'Name', @@ -48,10 +48,10 @@ const viewCmd = new Command('view') return; } logOutputObject(cmd.format, { - dataId: Number(file.dataId), + dataId: file.dataId, name: file.name, type: file.type, - size: Number(file.size), + size: file.size, hash: file.hash, supportedCountries: file.supportedCountries, supportedLanguages: file.supportedLanguages, diff --git a/src/cli/output.ts b/src/cli/output.ts index 7970cec..7b64c5e 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -11,14 +11,16 @@ function mapOutputObject(obj: FormattableObject, mapping: FieldMapping): Formatt return Object.fromEntries(mappedEntries); } +function jsonReplacer(key: string, value: any): any { + if (typeof value === 'bigint') { + return Number(value); + } + return value; +} + export function logOutputList(format: FormatOption, items: T[], mapping: FieldMapping = {}): void { if (format === 'json') { - console.log(JSON.stringify(items, (_, v) => { - if (typeof v === 'bigint') { - return v.toString(); - } - return v; - }, 2)); + console.log(JSON.stringify(items, jsonReplacer, 2)); return; } const mappedItems = items.map(item => mapOutputObject(item, mapping)); @@ -27,7 +29,7 @@ export function logOutputList(format: FormatOption, export function logOutputObject(format: FormatOption, obj: T, mapping: FieldMapping = {}): void { if (format === 'json') { - console.log(JSON.stringify(obj, null, 2)); + console.log(JSON.stringify(obj, jsonReplacer, 2)); return; } console.log(mapOutputObject(obj, mapping)); From 5d426bcf949a0a69249da7e76d00e2b9b6170bc4 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 23 Sep 2025 22:31:32 +0200 Subject: [PATCH 7/8] chore: refactor how outputs are created in CLI --- src/cli/apps.cmd.ts | 27 ++++++++++++++++-------- src/cli/files.cmd.ts | 24 ++++++++++++---------- src/cli/output.ts | 49 +++++++++++++++++++++++++++++++++----------- src/cli/tasks.cmd.ts | 16 ++++++++++----- 4 files changed, 80 insertions(+), 36 deletions(-) diff --git a/src/cli/apps.cmd.ts b/src/cli/apps.cmd.ts index 59f3b16..dd26b6b 100644 --- a/src/cli/apps.cmd.ts +++ b/src/cli/apps.cmd.ts @@ -7,13 +7,21 @@ const listCmd = new Command('ls') .action(commandHandler<[]>(async (cmd): Promise => { const ctx = getCliContext(); const { apps } = await ctx.grpc.listKnownBOSSApps({}); - logOutputList(cmd.format, apps.map(v => ({ - name: prettyTrunc(v.name, 20) - })), { - bossAppId: 'App ID', - name: 'Name', - titleId: 'Title ID', - titleRegion: 'Title region' + logOutputList(apps, { + format: cmd.format, + onlyIncludeKeys: ['bossAppId', 'name', 'tasks', 'titleRegion'], + mapping: { + bossAppId: 'App ID', + name: 'Name', + titleId: 'Title ID', + titleRegion: 'Title region' + }, + prettify(key, value) { + if (key === 'name') { + return prettyTrunc(value, 20); + } + return value; + } }); })); @@ -30,12 +38,15 @@ const viewCmd = new Command('view') return; } - logOutputObject(cmd.format, { + const obj = { appId: app.bossAppId, name: app.name, titleId: app.titleId, titleRegion: app.titleRegion, knownTasks: app.tasks + }; + logOutputObject(obj, { + format: cmd.format }); })); diff --git a/src/cli/files.cmd.ts b/src/cli/files.cmd.ts index c995130..5e51b0f 100644 --- a/src/cli/files.cmd.ts +++ b/src/cli/files.cmd.ts @@ -18,16 +18,16 @@ const listCmd = new Command('ls') bossAppId: appId, taskId: taskId }); - logOutputList(cmd.format, files.map(v => ({ - ...v, - size: v.size, - dataId: v.dataId - }), { - dataId: 'Data ID', - name: 'Name', - type: 'Type', - size: 'Size (bytes)' - })); + logOutputList(files, { + format: cmd.format, + onlyIncludeKeys: ['dataId', 'name', 'type', 'size'], + mapping: { + dataId: 'Data ID', + name: 'Name', + type: 'Type', + size: 'Size (bytes)' + } + }); })); const viewCmd = new Command('view') @@ -47,7 +47,7 @@ const viewCmd = new Command('view') console.log(`Could not find task file with data ID ${dataId} in task ${taskId}`); return; } - logOutputObject(cmd.format, { + logOutputObject({ dataId: file.dataId, name: file.name, type: file.type, @@ -62,6 +62,8 @@ const viewCmd = new Command('view') }, createdAt: new Date(Number(file.createdTimestamp)), updatedAt: new Date(Number(file.updatedTimestamp)) + }, { + format: cmd.format }); })); diff --git a/src/cli/output.ts b/src/cli/output.ts index 7b64c5e..0342157 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1,11 +1,25 @@ export type FormattableObject = Record; -export type FieldMapping = Record; export type FormatOption = 'json' | 'pretty'; -function mapOutputObject(obj: FormattableObject, mapping: FieldMapping): FormattableObject { +function preprocessObject(obj: FormattableObject, ops: FormattedOutputOptions): FormattableObject { + const onlyIncludeKeys = ops.onlyIncludeKeys; + if (!onlyIncludeKeys) { + return obj; + } + const entries = Object.entries(obj); - const mappedEntries = entries.map((v) => { - const newKey = mapping[v[0]] ?? v[0]; + const filteredEntries = entries.filter(v => onlyIncludeKeys.includes(v[0])); + return Object.fromEntries(filteredEntries); +} + +function makePrettyOutputObject(obj: FormattableObject, ops: FormattedOutputOptions): FormattableObject { + const entries = Object.entries(obj); + const prettifiedEntries = entries.map((v) => { + const value = ops.prettify ? ops.prettify(v[0], v[1]) : v[1]; + return [v[0], value]; + }); + const mappedEntries = prettifiedEntries.map((v) => { + const newKey = ops.mapping?.[v[0]] ?? v[0]; return [newKey, v[1]] as const; }); return Object.fromEntries(mappedEntries); @@ -18,19 +32,30 @@ function jsonReplacer(key: string, value: any): any { return value; } -export function logOutputList(format: FormatOption, items: T[], mapping: FieldMapping = {}): void { - if (format === 'json') { - console.log(JSON.stringify(items, jsonReplacer, 2)); +export type FormattedOutputOptions = { + format: FormatOption; + onlyIncludeKeys?: Array; + mapping?: Partial>; + prettify?: (key: string, value: any) => any; +}; + +export function logOutputList(items: T[], ops: FormattedOutputOptions): void { + const processedItems = items.map(v => preprocessObject(v, ops)); + if (ops.format === 'json') { + console.log(JSON.stringify(processedItems, jsonReplacer, 2)); return; } - const mappedItems = items.map(item => mapOutputObject(item, mapping)); + + const mappedItems = processedItems.map(item => makePrettyOutputObject(item, ops)); console.table(mappedItems); } -export function logOutputObject(format: FormatOption, obj: T, mapping: FieldMapping = {}): void { - if (format === 'json') { - console.log(JSON.stringify(obj, jsonReplacer, 2)); +export function logOutputObject(obj: T, ops: FormattedOutputOptions): void { + const processedObj = preprocessObject(obj, ops); + if (ops.format === 'json') { + console.log(JSON.stringify(processedObj, jsonReplacer, 2)); return; } - console.log(mapOutputObject(obj, mapping)); + + console.log(makePrettyOutputObject(processedObj, ops)); } diff --git a/src/cli/tasks.cmd.ts b/src/cli/tasks.cmd.ts index 432344d..ef41894 100644 --- a/src/cli/tasks.cmd.ts +++ b/src/cli/tasks.cmd.ts @@ -10,10 +10,14 @@ const listCmd = new Command('ls') const ctx = getCliContext(); const { tasks } = await ctx.grpc.listTasks({}); const filteredTasks = tasks.filter(v => v.bossAppId === appId); - logOutputList(cmd.format, filteredTasks, { - id: 'Task ID', - description: 'Description', - status: 'Status' + logOutputList(filteredTasks, { + format: cmd.format, + mapping: { + id: 'Task ID', + description: 'Description', + status: 'Status' + }, + onlyIncludeKeys: ['id', 'description', 'status'] }); })); @@ -30,7 +34,7 @@ const viewCmd = new Command('view') console.log(`Could not find task with ID ${taskId} in app ${appId}`); return; } - logOutputObject(cmd.format, { + logOutputObject({ taskId: task.id, inGameId: task.inGameId, description: task.description, @@ -40,6 +44,8 @@ const viewCmd = new Command('view') status: task.status, createdAt: new Date(Number(task.createdTimestamp)), updatedAt: new Date(Number(task.updatedTimestamp)) + }, { + format: cmd.format }); })); From c01bc7f167ba6a7d0ddbf93b3acb37488f5efa38 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 24 Sep 2025 14:59:13 +0200 Subject: [PATCH 8/8] fix: add missing table row in app ls --- src/cli/apps.cmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/apps.cmd.ts b/src/cli/apps.cmd.ts index dd26b6b..365ce72 100644 --- a/src/cli/apps.cmd.ts +++ b/src/cli/apps.cmd.ts @@ -9,7 +9,7 @@ const listCmd = new Command('ls') const { apps } = await ctx.grpc.listKnownBOSSApps({}); logOutputList(apps, { format: cmd.format, - onlyIncludeKeys: ['bossAppId', 'name', 'tasks', 'titleRegion'], + onlyIncludeKeys: ['bossAppId', 'name', 'titleId', 'titleRegion'], mapping: { bossAppId: 'App ID', name: 'Name',