From 67785bbb5b89833713973881b5584e7dd81167e8 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Tue, 6 Jan 2026 15:40:04 +0000 Subject: [PATCH 01/11] Replacing config parsing with our own parser lib --- package-lock.json | 29 ++-- package.json | 4 +- src/baseCommands/baseGameAndroidCommand.ts | 3 +- src/commands/game/ios/app/create.tsx | 3 +- src/utils/godot.ts | 168 +++++++-------------- src/utils/query/useAppleBundleId.ts | 2 +- 6 files changed, 71 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2079d7d..a9d1419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,11 +21,10 @@ "axios": "^1.7.7", "chalk": "^5.4.1", "crypto-js": "^4.2.0", - "deepmerge": "^4.3.1", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "ini": "^5.0.0", + "godot-export-presets": "^0.1.0", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", @@ -58,7 +57,6 @@ "@types/crypto-js": "^4.2.2", "@types/ejs": "^3.1.5", "@types/fs-extra": "^11.0.4", - "@types/ini": "^4.1.1", "@types/jsonwebtoken": "^9.0.6", "@types/luxon": "^3.4.2", "@types/mocha": "^10", @@ -5052,13 +5050,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8747,6 +8738,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/godot-export-presets": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/godot-export-presets/-/godot-export-presets-0.1.0.tgz", + "integrity": "sha512-OMaVmH+JBpICWoN9b+ETQZLmkRWVYkreCHN8bWHjAlt6ZuZXMPn/wZLT+Fd8Ib1erSeoWR0puA94hLKkQfJmYw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -9038,15 +9038,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/ink": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ink/-/ink-5.1.0.tgz", diff --git a/package.json b/package.json index cdd2c14..2473310 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,10 @@ "axios": "^1.7.7", "chalk": "^5.4.1", "crypto-js": "^4.2.0", - "deepmerge": "^4.3.1", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "ini": "^5.0.0", + "godot-export-presets": "^0.1.0", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", @@ -54,7 +53,6 @@ "@types/crypto-js": "^4.2.2", "@types/ejs": "^3.1.5", "@types/fs-extra": "^11.0.4", - "@types/ini": "^4.1.1", "@types/jsonwebtoken": "^9.0.6", "@types/luxon": "^3.4.2", "@types/mocha": "^10", diff --git a/src/baseCommands/baseGameAndroidCommand.ts b/src/baseCommands/baseGameAndroidCommand.ts index c4b2511..d4285f8 100644 --- a/src/baseCommands/baseGameAndroidCommand.ts +++ b/src/baseCommands/baseGameAndroidCommand.ts @@ -39,7 +39,8 @@ export abstract class BaseGameAndroidCommand extends B protected async getAndroidPackageName(gameId: string): Promise { const game = await this.getGame() const generated = generatePackageName(game.name) - const suggested = game.details?.iosBundleId || getGodotAndroidPackageName() || generated || 'com.example.game' + const godotPackageName = await getGodotAndroidPackageName() + const suggested = game.details?.iosBundleId || godotPackageName || generated || 'com.example.game' const question = `Please enter the Android Package Name, or press enter to use ${suggested}: ` const entered = await getInput(question) return entered || suggested diff --git a/src/commands/game/ios/app/create.tsx b/src/commands/game/ios/app/create.tsx index 9eedcd8..72d6937 100644 --- a/src/commands/game/ios/app/create.tsx +++ b/src/commands/game/ios/app/create.tsx @@ -40,8 +40,9 @@ export default class GameIosAppCreate extends BaseGameCommand => { if (bundleId) return bundleId const generatedBundleId = generatePackageName(game.name) + const godotBundleId = await getGodotAppleBundleIdentifier() const suggestedBundleId = - game.details?.iosBundleId || getGodotAppleBundleIdentifier() || generatedBundleId || 'com.example.game' + game.details?.iosBundleId || godotBundleId || generatedBundleId || 'com.example.game' const question = `Please enter the BundleId in the Apple Developer Portal, or press enter to use ${suggestedBundleId}: ` const enteredBundleId = await getInput(question) return enteredBundleId || suggestedBundleId diff --git a/src/utils/godot.ts b/src/utils/godot.ts index 731be63..fb98515 100644 --- a/src/utils/godot.ts +++ b/src/utils/godot.ts @@ -1,8 +1,16 @@ import fs from 'node:fs' import path from 'node:path' -import merge from 'deepmerge' -import {parse} from 'ini' +import { + ConfigFile, + findPreset, + getBasePreset, + getMajorVersion, + loadExportPresets, + mergePresets, + type Platform as GodotPlatform, + type GodotMajorVersion, +} from 'godot-export-presets' import {CapabilityType} from '@cli/apple/expo.js' import {Platform} from '@cli/types' @@ -33,10 +41,10 @@ export const GODOT_CAPABILITIES = [ ] // Tells us which capabilities are enabled in the Godot project -export function getGodotProjectCapabilities(platform: Platform) { - const exportPresets = getGodotExportPresets(platform) +export async function getGodotProjectCapabilities(platform: Platform) { + const exportPresets = await getGodotExportPresets(platform) - const options: Record = (exportPresets as any).options || {} + const options: Record = exportPresets.options || {} const capabilities = [] @@ -48,36 +56,41 @@ export function getGodotProjectCapabilities(platform: Platform) { return capabilities } -export function getGodotProjectConfig() { +export function getGodotProjectConfig(): ConfigFile { const cwd = process.cwd() const projectGodotPath = path.join(cwd, 'project.godot') const projectGodotContent = fs.readFileSync(projectGodotPath, 'utf8') - return parse(projectGodotContent) + const configFile = new ConfigFile() + const error = configFile.parse(projectGodotContent) + if (error) { + throw error + } + return configFile } export function getGodotProjectName(): null | string { try { const projectGodotConfig = getGodotProjectConfig() - return projectGodotConfig.application['config/name'] + return (projectGodotConfig.get_value('application', 'config/name') as string) || null } catch { return null } } -export function getGodotAppleBundleIdentifier(): null | string { +export async function getGodotAppleBundleIdentifier(): Promise { try { - const preset = getGodotExportPresets(Platform.IOS) - return (preset.options as any)['application/bundle_identifier'] + const preset = await getGodotExportPresets(Platform.IOS) + return (preset.options?.['application/bundle_identifier'] as string) || null } catch (error) { console.log(error) return null } } -export function getGodotAndroidPackageName(): null | string { +export async function getGodotAndroidPackageName(): Promise { try { - const preset = getGodotExportPresets(Platform.ANDROID) - return (preset.options as any)['package/unique_name'] + const preset = await getGodotExportPresets(Platform.ANDROID) + return (preset.options?.['package/unique_name'] as string) || null } catch (error) { console.log(error) return null @@ -87,24 +100,32 @@ export function getGodotAndroidPackageName(): null | string { // TODO: is there a more reliable way to get the Godot version? export function getGodotVersion(): string { const projectGodotConfig = getGodotProjectConfig() - if ('config/features' in projectGodotConfig.application) { - const features = projectGodotConfig.application['config/features'] + const features = projectGodotConfig.get_value('application', 'config/features') as string | undefined + if (features) { // config/features=PackedStringArray("4.3") // config/features=PackedStringArray("4.2", "GL Compatibility") const match = features.match(/"(\d+\.\d+)"/) - if (!match) throw new Error("Couldn't find Godot version in project.godot") - return match[1] + if (match) { + return match[1] + } } return '3.6' } // TODO: any differences in the presets between v 3.X and 4.X? -export function getGodotExportPresets(platform: Platform) { +export async function getGodotExportPresets(platform: Platform) { const {warn} = console - // Get the base config for this preset - let presetConfig = platform === Platform.IOS ? getBaseExportPresets_iOS() : getBaseExportPresets_Android() + // Get Godot version to determine which base preset to use + const godotVersion = getGodotVersion() + const majorVersion = getMajorVersion(godotVersion) as GodotMajorVersion + + // Convert Platform enum to Godot platform string + const godotPlatform: GodotPlatform = platform === Platform.IOS ? 'iOS' : 'Android' + + // Get the base preset for this platform and version + let presetConfig = getBasePreset(godotPlatform, majorVersion) // Get the preset options from any export_presets.cfg if found const cwd = process.cwd() @@ -113,20 +134,19 @@ export function getGodotExportPresets(platform: Platform) { const isFound = fs.existsSync(exportPresetsPath) if (isFound) { - const exportPresetsContent = fs.readFileSync(exportPresetsPath, 'utf8') - const exportPresetsIni = parse(exportPresetsContent) - // Find the preset with the same name in the existing config - const presetIndexes = Object.keys(exportPresetsIni.preset || {}) - const presetIndex = presetIndexes.find((index) => { - const current = exportPresetsIni.preset[index] - return `${current.name}`.toUpperCase() === platform - }) - - if (presetIndex) { - // Merge the preset options base config - presetConfig = merge(presetConfig, exportPresetsIni.preset[presetIndex]) as any - } else { - warn(`Preset ${platform} not found in ${filename} - will use defaults`) + try { + const exportPresets = await loadExportPresets(exportPresetsPath) + // Find the preset with the same platform + const foundPreset = findPreset(exportPresets, {platform: godotPlatform}) + + if (foundPreset) { + // Merge the preset with base config + presetConfig = mergePresets(presetConfig, foundPreset) + } else { + warn(`Preset ${platform} not found in ${filename} - will use defaults`) + } + } catch (error) { + warn(`Error loading ${filename}: ${error}`) } } else { warn(`${filename} not found at ${exportPresetsPath}`) @@ -135,81 +155,3 @@ export function getGodotExportPresets(platform: Platform) { return presetConfig } -// TODO: type this properly (including missing options) -function getBaseExportPresets_iOS() { - return { - custom_features: '', - dedicated_server: false, - encrypt_directory: false, - encrypt_pck: false, - encryption_exclude_filters: '', - encryption_include_filters: '', - exclude_filter: '', - export_filter: 'all_resources', - export_path: 'output', - include_filter: '', - name: 'iOS', - options: { - 'application/export_project_only': true, - 'application/icon_interpolation': '4', - 'application/launch_screens_interpolation': '4', - 'application/short_version': '1.0.0', // default version number - 'application/signature': '', - 'architectures/arm64': true, - 'capabilities/access_wifi': false, - 'capabilities/push_notifications': false, - 'custom_template/debug': '', - 'custom_template/release': '', - 'icons/app_store_1024x1024': '', - 'icons/ipad_76x76': '', - 'icons/ipad_152x152': '', - 'icons/ipad_167x167': '', - 'icons/iphone_120x120': '', - 'icons/iphone_180x180': '', - 'icons/notification_40x40': '', - 'icons/notification_60x60': '', - 'icons/settings_58x58': '', - 'icons/settings_87x87': '', - 'icons/spotlight_40x40': '', - 'icons/spotlight_80x80': '', - 'landscape_launch_screens/ipad_1024x768': '', - 'landscape_launch_screens/ipad_2048x1536': '', - 'landscape_launch_screens/iphone_2208x1242': '', - 'landscape_launch_screens/iphone_2436x1125': '', - 'portrait_launch_screens/ipad_768x1024': '', - 'portrait_launch_screens/ipad_1536x2048': '', - 'portrait_launch_screens/iphone_640x960': '', - 'portrait_launch_screens/iphone_640x1136': '', - 'portrait_launch_screens/iphone_750x1334': '', - 'portrait_launch_screens/iphone_1125x2436': '', - 'portrait_launch_screens/iphone_1242x2208': '', - 'privacy/camera_usage_description': '', - 'privacy/camera_usage_description_localized': '{}', - 'privacy/microphone_usage_description': '', - 'privacy/microphone_usage_description_localized': '{}', - 'privacy/photolibrary_usage_description': '', - 'privacy/photolibrary_usage_description_localized': '{}', - 'storyboard/custom_bg_color': 'Color(0, 0, 0, 1)', - 'storyboard/custom_image@2x': '', - 'storyboard/custom_image@3x': '', - 'storyboard/image_scale_mode': '0', - 'storyboard/use_custom_bg_color': false, - 'storyboard/use_launch_screen_storyboard': true, - 'user_data/accessible_from_files_app': false, - 'user_data/accessible_from_itunes_sharing': false, - }, - platform: 'iOS', - runnable: true, - } -} - -function getBaseExportPresets_Android() { - return { - name: 'Android', - // TODO - options: { - // TODO - }, - platform: 'Android', - } -} diff --git a/src/utils/query/useAppleBundleId.ts b/src/utils/query/useAppleBundleId.ts index 2c1e5a4..6eb1d92 100644 --- a/src/utils/query/useAppleBundleId.ts +++ b/src/utils/query/useAppleBundleId.ts @@ -58,7 +58,7 @@ export const fetchBundleId = async ({ctx, iosBundleId}: AppleBundleIdQueryProps) if (!bundleId) return empty const bundleIdCapabilities = await getBundleIdCapabilities(bundleId) - const projectCapabilities = getGodotProjectCapabilities(Platform.IOS) + const projectCapabilities = await getGodotProjectCapabilities(Platform.IOS) const capabilitiesTable = GODOT_CAPABILITIES.map((gc) => { const isEnabledInBundle = bundleIdCapabilities.includes(gc.type) From 8fa72f44e2c77985be9a89f16b78b23a50ecc262 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Tue, 6 Jan 2026 15:59:52 +0000 Subject: [PATCH 02/11] Updated to fixed version of godot-export-presets --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9d1419..f468d66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "godot-export-presets": "^0.1.0", + "godot-export-presets": "^0.1.4", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", @@ -8739,9 +8739,9 @@ } }, "node_modules/godot-export-presets": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/godot-export-presets/-/godot-export-presets-0.1.0.tgz", - "integrity": "sha512-OMaVmH+JBpICWoN9b+ETQZLmkRWVYkreCHN8bWHjAlt6ZuZXMPn/wZLT+Fd8Ib1erSeoWR0puA94hLKkQfJmYw==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/godot-export-presets/-/godot-export-presets-0.1.4.tgz", + "integrity": "sha512-+GziEeXCXlg2/P/4bcDEz7Q2tNAjknmDmmp6OPUFhZlJzLZZcTRZlfZYc2qmAWTiXnIIvLDGSm9IMpbI7lUZrQ==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 2473310..6484a95 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "godot-export-presets": "^0.1.0", + "godot-export-presets": "^0.1.4", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", From 81f360a0483da32038663434e293480973764a5a Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Tue, 6 Jan 2026 16:16:45 +0000 Subject: [PATCH 03/11] Refactoring the create initial build step --- .../InitialAndroidBuild.tsx | 97 +++++++++++++++++++ .../android/CreateInitialBuild/index.tsx | 96 +----------------- 2 files changed, 102 insertions(+), 91 deletions(-) create mode 100644 src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx diff --git a/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx new file mode 100644 index 0000000..0c4b84b --- /dev/null +++ b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx @@ -0,0 +1,97 @@ +import {Box, Text} from 'ink' +import Spinner from 'ink-spinner' +import {useContext, useEffect, useRef, useState} from 'react' + +import {CommandContext, JobLogTail, JobProgress, Markdown, StepProps} from '@cli/components/index.js' +import {WEB_URL} from '@cli/constants/config.js' +import {Job, JobStatus, Platform} from '@cli/types/api.js' +import {getShortUUID, useBuilds, useJobs, useShip} from '@cli/utils/index.js' + +export interface InitialAndroidBuildProps extends StepProps { + gameId: string +} + +export const InitialAndroidBuild = ({gameId, onComplete, onError, ...boxProps}: InitialAndroidBuildProps) => { + const {command} = useContext(CommandContext) + const {data: buildData, isLoading: isLoadingBuilds} = useBuilds({pageNumber: 0, projectId: gameId}) + const {data: jobData, isLoading: isLoadingJobs} = useJobs({ + pageNumber: 0, + projectId: gameId, + }) + const prevHasBuild = useRef(false) + const shipMutation = useShip() + const [shipLog, setShipLog] = useState('') + const [failedJob, setFailedJob] = useState(null) + + // Trigger a build if we don't have one + useEffect(() => { + if (isLoadingBuilds || isLoadingJobs) return + if (!buildData) return + if (!jobData) return + if (!command) return + + const hasAndroidBuild = buildData.data.some((build) => build.platform === Platform.ANDROID) + // If we now have a build - trigger the onComplete + if (!prevHasBuild.current && hasAndroidBuild) return onComplete() + prevHasBuild.current = hasAndroidBuild + const hasRunningAndroidJob = jobData.data.some( + (job) => job.type === Platform.ANDROID && [JobStatus.PENDING, JobStatus.PROCESSING].includes(job.status), + ) + // If we don't have a build and we don't have an android job - run the ship command + const shouldRun = !hasAndroidBuild && !hasRunningAndroidJob + if (shouldRun) + shipMutation + .mutateAsync({ + command, + log: setShipLog, + shipFlags: { + platform: 'android', + skipPublish: true, + }, + }) + .catch(onError) + }, [buildData, jobData, command]) + + const androidJob = jobData?.data.find( + (job) => job.type === Platform.ANDROID && [JobStatus.PENDING, JobStatus.PROCESSING].includes(job.status), + ) + + return ( + <> + + + Create an initial build... + {(isLoadingBuilds || isLoadingJobs || shipMutation.isPending) && } + + {androidJob === null && {shipLog}} + {androidJob && ( + { + setFailedJob(j) + // Wait before triggering the error to allow the job log to be displayed + setTimeout(() => { + onError(new Error(`Job ${j.id} failed`)) + }, 1000) + }} + /> + )} + + {failedJob && ( + <> + + + + + + )} + + + ) +} diff --git a/src/components/android/CreateInitialBuild/index.tsx b/src/components/android/CreateInitialBuild/index.tsx index 6fa6e38..2f209c7 100644 --- a/src/components/android/CreateInitialBuild/index.tsx +++ b/src/components/android/CreateInitialBuild/index.tsx @@ -1,96 +1,10 @@ -import {Box, Text} from 'ink' -import Spinner from 'ink-spinner' -import {useContext, useEffect, useRef, useState} from 'react' +import {useContext} from 'react' -import {CommandContext, GameContext, JobLogTail, JobProgress, Markdown, StepProps} from '@cli/components/index.js' -import {WEB_URL} from '@cli/constants/config.js' -import {Job, JobStatus, Platform} from '@cli/types/api.js' -import {getShortUUID, useBuilds, useJobs, useShip} from '@cli/utils/index.js' +import {GameContext, StepProps} from '@cli/components/index.js' + +import {InitialAndroidBuild} from './InitialAndroidBuild.js' export const CreateInitialBuild = (props: StepProps): JSX.Element => { const {gameId} = useContext(GameContext) - return <>{gameId && } -} - -interface CreateForGameProps extends StepProps { - gameId: string -} - -const CreateForGame = ({gameId, onComplete, onError, ...boxProps}: CreateForGameProps) => { - const {command} = useContext(CommandContext) - const {data: buildData, isLoading: isLoadingBuilds} = useBuilds({pageNumber: 0, projectId: gameId}) - const {data: jobData, isLoading: isLoadingJobs} = useJobs({ - pageNumber: 0, - projectId: gameId, - }) - const prevHasBuild = useRef(false) - const shipMutation = useShip() - const [shipLog, setShipLog] = useState('') - const [failedJob, setFailedJob] = useState(null) - - // Trigger a build if we don't have one - useEffect(() => { - if (isLoadingBuilds || isLoadingJobs) return - if (!buildData) return - if (!jobData) return - if (!command) return - - const hasAndroidBuild = buildData.data.some((build) => build.platform === Platform.ANDROID) - // If we now have a build - trigger the onComplete - if (!prevHasBuild.current && hasAndroidBuild) return onComplete() - prevHasBuild.current = hasAndroidBuild - const hasRunningAndroidJob = jobData.data.some( - (job) => job.type === Platform.ANDROID && [JobStatus.PENDING, JobStatus.PROCESSING].includes(job.status), - ) - // If we don't have a build and we don't have an android job - run the ship command - const shouldRun = !hasAndroidBuild && !hasRunningAndroidJob - if (shouldRun) - shipMutation - .mutateAsync({ - command, - log: setShipLog, - shipFlags: { - platform: 'android', - skipPublish: true - } - }) - .catch(onError) - }, [buildData, jobData, command]) - - const androidJob = jobData?.data.find( - (job) => job.type === Platform.ANDROID && [JobStatus.PENDING, JobStatus.PROCESSING].includes(job.status), - ) - - return ( - <> - - - Create an initial build... - {(isLoadingBuilds || isLoadingJobs || shipMutation.isPending) && } - - {androidJob === null && {shipLog}} - {androidJob && { - setFailedJob(j) - // Wait before triggering the error to allow the job log to be displayed - setTimeout(() => { - onError(new Error(`Job ${j.id} failed`)) - }, 1000) - }}/>} - - {failedJob && ( - <> - - - - - - )} - - - ) + return <>{gameId && } } From 3af617447023a67d744d991251f14a9512f2032c Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Tue, 6 Jan 2026 16:31:22 +0000 Subject: [PATCH 04/11] Initial build must be AAB --- src/components/android/AndroidWizard/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/android/AndroidWizard/utils.ts b/src/components/android/AndroidWizard/utils.ts index 5013b55..b4e4ec9 100644 --- a/src/components/android/AndroidWizard/utils.ts +++ b/src/components/android/AndroidWizard/utils.ts @@ -1,6 +1,6 @@ import {getGoogleStatus, getProject, getProjectCredentials} from '@cli/api/index.js' import {BaseCommand} from '@cli/baseCommands/baseCommand.js' -import {CredentialsType, Platform} from '@cli/types/index.js' +import {BuildType, CredentialsType, Platform} from '@cli/types/index.js' import {KeyTestError, KeyTestStatus, fetchKeyTestResult, queryBuilds} from '@cli/utils/index.js' export enum StepStatus { @@ -91,7 +91,9 @@ export const getStatusFlags = async (cmd: BaseCommand): Promise build.platform === Platform.ANDROID)) + const hasInitialBuild = Boolean( + buildsResponse?.data?.some((build) => build.platform === Platform.ANDROID && build.buildType === BuildType.AAB), + ) const testResult = projectId ? await fetchKeyTestResult({projectId}) : null From 4a6bd67b9b1827eb09f5f115dda28c6cfaec1032 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Wed, 7 Jan 2026 11:16:06 +0000 Subject: [PATCH 05/11] Functions and util command to get and set the build method --- package-lock.json | 8 +-- package.json | 2 +- src/commands/util/android-build-method.ts | 40 +++++++++++++ src/utils/godot.ts | 72 ++++++++++++++++++----- 4 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 src/commands/util/android-build-method.ts diff --git a/package-lock.json b/package-lock.json index f468d66..3ba9a02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "godot-export-presets": "^0.1.4", + "godot-export-presets": "^0.1.6", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", @@ -8739,9 +8739,9 @@ } }, "node_modules/godot-export-presets": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/godot-export-presets/-/godot-export-presets-0.1.4.tgz", - "integrity": "sha512-+GziEeXCXlg2/P/4bcDEz7Q2tNAjknmDmmp6OPUFhZlJzLZZcTRZlfZYc2qmAWTiXnIIvLDGSm9IMpbI7lUZrQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/godot-export-presets/-/godot-export-presets-0.1.6.tgz", + "integrity": "sha512-q1i1C8A991iJ5y3eqbfG6kKsNEEKdFxEwazikolNxP63eN6dd7TZQYHKHfSCF2HiVMt3GXQQtd3QzUdy3LFXjA==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 6484a95..55a3540 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "fullscreen-ink": "^0.1.0", - "godot-export-presets": "^0.1.4", + "godot-export-presets": "^0.1.6", "ink": "^5.0.1", "ink-spinner": "^5.0.0", "isomorphic-git": "^1.27.1", diff --git a/src/commands/util/android-build-method.ts b/src/commands/util/android-build-method.ts new file mode 100644 index 0000000..dfaa3b7 --- /dev/null +++ b/src/commands/util/android-build-method.ts @@ -0,0 +1,40 @@ +import {Flags} from '@oclif/core' + +import {BaseCommand} from '@cli/baseCommands/index.js' +import {isGradleBuildEnabled, setGradleBuildEnabled} from '@cli/utils/godot.js' + +export default class UtilAndroidBuildMethod extends BaseCommand { + static override args = {} + static override description = 'Gets and sets the Android build method in export_presets.cfg' + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --legacy', + '<%= config.bin %> <%= command.id %> --gradle', + ] + + static override flags = { + legacy: Flags.boolean({char: 'l', description: 'use legacy build method'}), + gradle: Flags.boolean({char: 'g', description: 'use gradle build method'}), + } + + public async run(): Promise { + const {flags} = await this.parse(UtilAndroidBuildMethod) + + if (flags.legacy && flags.gradle) { + this.error('Cannot use both --legacy and --gradle flags together') + } + + if (!flags.legacy && !flags.gradle) { + // Show current build method + const buildMethod = (await isGradleBuildEnabled()) ? 'gradle' : 'legacy' + this.log(`Current Android build method: ${buildMethod}`) + return + } + + const isGradle = flags.gradle ? true : false + const buildMethod = flags.legacy ? 'legacy' : 'gradle' + this.log(`Setting Android build method to: ${buildMethod}`) + // Set the build method in the export presets file + await setGradleBuildEnabled(isGradle) + } +} diff --git a/src/utils/godot.ts b/src/utils/godot.ts index fb98515..3c22cd8 100644 --- a/src/utils/godot.ts +++ b/src/utils/godot.ts @@ -10,12 +10,13 @@ import { mergePresets, type Platform as GodotPlatform, type GodotMajorVersion, + saveExportPresets, + ExportPresetsFile, } from 'godot-export-presets' import {CapabilityType} from '@cli/apple/expo.js' import {Platform} from '@cli/types' - // Check if the current working directory is a Godot game // TODO: allow for cwd override export function isCWDGodotGame(): boolean { @@ -100,17 +101,20 @@ export async function getGodotAndroidPackageName(): Promise { // TODO: is there a more reliable way to get the Godot version? export function getGodotVersion(): string { const projectGodotConfig = getGodotProjectConfig() - const features = projectGodotConfig.get_value('application', 'config/features') as string | undefined - if (features) { - // config/features=PackedStringArray("4.3") - // config/features=PackedStringArray("4.2", "GL Compatibility") - const match = features.match(/"(\d+\.\d+)"/) - if (match) { - return match[1] - } + const features = projectGodotConfig.get_value('application', 'config/features') as string[] + if (!features || features.length === 0) { + return '3.6' } + const [version] = features + return version as string +} - return '3.6' +export function getExportPresetsPath(): string { + // Get the preset options from any export_presets.cfg if found + const cwd = process.cwd() + const filename = 'export_presets.cfg' + const exportPresetsPath = path.join(cwd, filename) + return exportPresetsPath } // TODO: any differences in the presets between v 3.X and 4.X? @@ -128,9 +132,7 @@ export async function getGodotExportPresets(platform: Platform) { let presetConfig = getBasePreset(godotPlatform, majorVersion) // Get the preset options from any export_presets.cfg if found - const cwd = process.cwd() - const filename = 'export_presets.cfg' - const exportPresetsPath = path.join(cwd, filename) + const exportPresetsPath = getExportPresetsPath() const isFound = fs.existsSync(exportPresetsPath) if (isFound) { @@ -143,15 +145,53 @@ export async function getGodotExportPresets(platform: Platform) { // Merge the preset with base config presetConfig = mergePresets(presetConfig, foundPreset) } else { - warn(`Preset ${platform} not found in ${filename} - will use defaults`) + warn(`Preset ${platform} not found in ${exportPresetsPath} - will use defaults`) } } catch (error) { - warn(`Error loading ${filename}: ${error}`) + warn(`Error loading ${exportPresetsPath}: ${error} - will use defaults`) } } else { - warn(`${filename} not found at ${exportPresetsPath}`) + warn(`Export presets not found at ${exportPresetsPath} - will use defaults`) } return presetConfig } +export function getGradleBuildOptionKey(majorVersion: GodotMajorVersion): string { + return majorVersion === 4 ? 'gradle_build/use_gradle_build' : 'custom_build/use_custom_build' +} + +// Tells us if Gradle build is enabled in export_presets.cfg +// This uses getGodotExportPresets which uses the base preset if no config file +// The base preset has Gradle enabled by default +export async function isGradleBuildEnabled(): Promise { + const godotVersion = getGodotVersion() + const majorVersion = getMajorVersion(godotVersion) as GodotMajorVersion + const preset = await getGodotExportPresets(Platform.ANDROID) + const buildOptionKey = getGradleBuildOptionKey(majorVersion) + const isEnabled = preset.options?.[buildOptionKey] + return isEnabled === true || isEnabled === 'true' +} + +// Sets the Gradle build option in export_presets.cfg +// If the file does not exist, it will be created +export async function setGradleBuildEnabled(value: boolean): Promise { + const exportPresetsPath = getExportPresetsPath() + let exportPresets: ExportPresetsFile = {presets: []} + if (fs.existsSync(exportPresetsPath)) { + exportPresets = await loadExportPresets(exportPresetsPath) + } else { + console.warn(`Export presets not found at ${exportPresetsPath} - creating new file`) + } + const godotVersion = getGodotVersion() + const majorVersion = getMajorVersion(godotVersion) as GodotMajorVersion + let androidPreset = findPreset(exportPresets, {platform: 'Android'}) + if (!androidPreset) { + androidPreset = getBasePreset('Android', majorVersion) + exportPresets.presets.push(androidPreset) + } + const buildOptionKey = getGradleBuildOptionKey(majorVersion) + androidPreset.options = androidPreset.options || {} + androidPreset.options[buildOptionKey] = value + await saveExportPresets(exportPresetsPath, exportPresets) +} From 2f0e9897d9d772e67494168947264083b42eae70 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Wed, 7 Jan 2026 14:37:42 +0000 Subject: [PATCH 06/11] Detect build method in the wizard and prompt for change --- ...confirm-change-android-build-method.md.ejs | 13 ++++ .../InitialAndroidBuild.tsx | 4 +- .../android/CreateInitialBuild/index.tsx | 75 ++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 assets/markdown/confirm-change-android-build-method.md.ejs diff --git a/assets/markdown/confirm-change-android-build-method.md.ejs b/assets/markdown/confirm-change-android-build-method.md.ejs new file mode 100644 index 0000000..157d00e --- /dev/null +++ b/assets/markdown/confirm-change-android-build-method.md.ejs @@ -0,0 +1,13 @@ +# Confirm change to export_presets.cfg + +**In order to publish your game on Google Play, an edit must be made to your `export_presets.cfg` file to enable the Gradle build method.** + +This change is necessary because Google Play requires Android App Bundles (AAB files) for new apps, and the Gradle build method is needed to create AAB files. + +You can read more about this in the **ShipThis Documentation** [<%= docsURL %>](<%= docsURL %>) + +You are using Godot version **<%= godotVersion %>**, ShipThis will update the **<%= optionKey %>** option in your `export_presets.cfg` file to enable the Gradle build method. + +## Do you want to proceed with this change? + +Please press **Y** to confirm and proceed with the change or **N** to cancel the operation and exit the Android wizard. \ No newline at end of file diff --git a/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx index 0c4b84b..86a1530 100644 --- a/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx +++ b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx @@ -4,7 +4,7 @@ import {useContext, useEffect, useRef, useState} from 'react' import {CommandContext, JobLogTail, JobProgress, Markdown, StepProps} from '@cli/components/index.js' import {WEB_URL} from '@cli/constants/config.js' -import {Job, JobStatus, Platform} from '@cli/types/api.js' +import {BuildType, Job, JobStatus, Platform} from '@cli/types/api.js' import {getShortUUID, useBuilds, useJobs, useShip} from '@cli/utils/index.js' export interface InitialAndroidBuildProps extends StepProps { @@ -30,7 +30,7 @@ export const InitialAndroidBuild = ({gameId, onComplete, onError, ...boxProps}: if (!jobData) return if (!command) return - const hasAndroidBuild = buildData.data.some((build) => build.platform === Platform.ANDROID) + const hasAndroidBuild = buildData.data.some((build) => build.platform === Platform.ANDROID && build.buildType == BuildType.AAB) // If we now have a build - trigger the onComplete if (!prevHasBuild.current && hasAndroidBuild) return onComplete() prevHasBuild.current = hasAndroidBuild diff --git a/src/components/android/CreateInitialBuild/index.tsx b/src/components/android/CreateInitialBuild/index.tsx index 2f209c7..1db7618 100644 --- a/src/components/android/CreateInitialBuild/index.tsx +++ b/src/components/android/CreateInitialBuild/index.tsx @@ -1,10 +1,77 @@ -import {useContext} from 'react' +import {useContext, useEffect, useState} from 'react' +import {Box, Text} from 'ink' +import {getMajorVersion} from 'godot-export-presets' -import {GameContext, StepProps} from '@cli/components/index.js' +import {GameContext, Markdown, StepProps} from '@cli/components/index.js' +import { + getGodotVersion, + getGradleBuildOptionKey, + isGradleBuildEnabled, + setGradleBuildEnabled, +} from '@cli/utils/godot.js' import {InitialAndroidBuild} from './InitialAndroidBuild.js' +import {WEB_URL} from '@cli/constants/index.js' +import {useSafeInput} from '@cli/utils/index.js' -export const CreateInitialBuild = (props: StepProps): JSX.Element => { +interface Props extends React.ComponentPropsWithoutRef { + onConfirm?: () => void + onCancel?: () => void +} + +const EnableGradle = ({onConfirm, onCancel, ...boxProps}: Props) => { + const godotVersion = getGodotVersion() + const majorVersion = getMajorVersion(godotVersion) + + useSafeInput(async (input) => { + if (input == 'y') return onConfirm && onConfirm() + if (input == 'n') return onCancel && onCancel() + }) + + return ( + + + + + + ) +} + +export const CreateInitialBuild = ({onComplete, onError, ...boxProps}: StepProps): JSX.Element => { const {gameId} = useContext(GameContext) - return <>{gameId && } + + const [willBuildAAB, setWillBuildAAB] = useState(null) + + useEffect(() => { + // We can only build AABs if Gradle build is enabled + async function fetchBuildMethod() { + const isGradle = await isGradleBuildEnabled() + setWillBuildAAB(isGradle) + } + fetchBuildMethod() + }, []) + + const updateBuildMethod = async () => { + await setGradleBuildEnabled(true) + setWillBuildAAB(true) + } + + if (willBuildAAB === null) { + return Loading... + } + + if (willBuildAAB === false) { + return updateBuildMethod()} onCancel={() => process.exit()} {...boxProps} /> + } + + return ( + <>{gameId && } + ) } From 708ec49353f105528577c28b0eec2c94e4e9330f Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Wed, 7 Jan 2026 14:45:12 +0000 Subject: [PATCH 07/11] Small refactor --- .../CreateInitialBuild/EnableGradle.tsx | 38 ++++++++++++ .../android/CreateInitialBuild/index.tsx | 58 ++++--------------- 2 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 src/components/android/CreateInitialBuild/EnableGradle.tsx diff --git a/src/components/android/CreateInitialBuild/EnableGradle.tsx b/src/components/android/CreateInitialBuild/EnableGradle.tsx new file mode 100644 index 0000000..5c3c2dd --- /dev/null +++ b/src/components/android/CreateInitialBuild/EnableGradle.tsx @@ -0,0 +1,38 @@ +import {Box} from 'ink' +import {getMajorVersion} from 'godot-export-presets' + +import {Markdown} from '@cli/components/index.js' +import {getGodotVersion, getGradleBuildOptionKey} from '@cli/utils/godot.js' + +import {WEB_URL} from '@cli/constants/index.js' +import {useSafeInput} from '@cli/utils/index.js' + +interface Props extends React.ComponentPropsWithoutRef { + onConfirm?: () => void + onCancel?: () => void +} + +export const EnableGradle = ({onConfirm, onCancel, ...boxProps}: Props) => { + const godotVersion = getGodotVersion() + const majorVersion = getMajorVersion(godotVersion) + + useSafeInput(async (input) => { + if (input == 'y') return onConfirm && onConfirm() + if (input == 'n') return onCancel && onCancel() + }) + + return ( + + + + + + ) +} diff --git a/src/components/android/CreateInitialBuild/index.tsx b/src/components/android/CreateInitialBuild/index.tsx index 1db7618..67d30d8 100644 --- a/src/components/android/CreateInitialBuild/index.tsx +++ b/src/components/android/CreateInitialBuild/index.tsx @@ -1,74 +1,36 @@ import {useContext, useEffect, useState} from 'react' -import {Box, Text} from 'ink' -import {getMajorVersion} from 'godot-export-presets' +import {Text} from 'ink' -import {GameContext, Markdown, StepProps} from '@cli/components/index.js' -import { - getGodotVersion, - getGradleBuildOptionKey, - isGradleBuildEnabled, - setGradleBuildEnabled, -} from '@cli/utils/godot.js' +import {GameContext, StepProps} from '@cli/components/index.js' +import {isGradleBuildEnabled, setGradleBuildEnabled} from '@cli/utils/godot.js' import {InitialAndroidBuild} from './InitialAndroidBuild.js' -import {WEB_URL} from '@cli/constants/index.js' -import {useSafeInput} from '@cli/utils/index.js' - -interface Props extends React.ComponentPropsWithoutRef { - onConfirm?: () => void - onCancel?: () => void -} - -const EnableGradle = ({onConfirm, onCancel, ...boxProps}: Props) => { - const godotVersion = getGodotVersion() - const majorVersion = getMajorVersion(godotVersion) - - useSafeInput(async (input) => { - if (input == 'y') return onConfirm && onConfirm() - if (input == 'n') return onCancel && onCancel() - }) - - return ( - - - - - - ) -} +import {EnableGradle} from './EnableGradle.js' export const CreateInitialBuild = ({onComplete, onError, ...boxProps}: StepProps): JSX.Element => { const {gameId} = useContext(GameContext) - - const [willBuildAAB, setWillBuildAAB] = useState(null) + const [canBuildAAB, setCanBuildAAB] = useState(null) useEffect(() => { // We can only build AABs if Gradle build is enabled async function fetchBuildMethod() { const isGradle = await isGradleBuildEnabled() - setWillBuildAAB(isGradle) + setCanBuildAAB(isGradle) } fetchBuildMethod() }, []) const updateBuildMethod = async () => { await setGradleBuildEnabled(true) - setWillBuildAAB(true) + setCanBuildAAB(true) } - if (willBuildAAB === null) { + if (canBuildAAB === null) { return Loading... } - if (willBuildAAB === false) { - return updateBuildMethod()} onCancel={() => process.exit()} {...boxProps} /> + if (canBuildAAB === false) { + return updateBuildMethod()} onCancel={() => process.exit()} /> } return ( From 19c0bac4248d3d996ff222996b79acfc7f2ca91c Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Thu, 8 Jan 2026 15:50:26 +0000 Subject: [PATCH 08/11] Show last job warning below the JobProgress %age shown on shipthis game ship command --- src/components/JobLogTail.tsx | 27 ++++---------- .../InitialAndroidBuild.tsx | 4 ++- src/components/common/JobLogLine.tsx | 35 +++++++++++++++++++ src/components/common/JobProgress.tsx | 26 +++++++++++--- src/components/common/index.ts | 3 +- 5 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 src/components/common/JobLogLine.tsx diff --git a/src/components/JobLogTail.tsx b/src/components/JobLogTail.tsx index deff3a3..7c141b3 100644 --- a/src/components/JobLogTail.tsx +++ b/src/components/JobLogTail.tsx @@ -1,39 +1,24 @@ -import {Box, Text} from 'ink' +import {Box} from 'ink' import Spinner from 'ink-spinner' import {JobLogEntry} from '@cli/types' import {JobLogTailProps, useJobLogTail} from '@cli/utils/hooks/index.js' -import {getMessageColor, getShortTime, getStageColor} from '@cli/utils/index.js' import {Title} from './common/Title.js' -import {TruncatedText} from './common/TruncatedText.js' + +import {JobLogLine} from './common/JobLogLine.js' export const JobLogTail = (props: JobLogTailProps) => { const {data, isLoading} = useJobLogTail(props) - // TODO: the is causing issues return ( Job Logs {isLoading && } - {data.map((log: JobLogEntry) => { - const stageColor = getStageColor(log.stage) - const messageColor = getMessageColor(log.level) - return ( - - - {getShortTime(log.sentAt)} - - - {log.stage} - - - {log.message} - - - ) - })} + {data.map((log: JobLogEntry) => ( + + ))} ) diff --git a/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx index 86a1530..598f753 100644 --- a/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx +++ b/src/components/android/CreateInitialBuild/InitialAndroidBuild.tsx @@ -30,7 +30,9 @@ export const InitialAndroidBuild = ({gameId, onComplete, onError, ...boxProps}: if (!jobData) return if (!command) return - const hasAndroidBuild = buildData.data.some((build) => build.platform === Platform.ANDROID && build.buildType == BuildType.AAB) + const hasAndroidBuild = buildData.data.some( + (build) => build.platform === Platform.ANDROID && build.buildType == BuildType.AAB, + ) // If we now have a build - trigger the onComplete if (!prevHasBuild.current && hasAndroidBuild) return onComplete() prevHasBuild.current = hasAndroidBuild diff --git a/src/components/common/JobLogLine.tsx b/src/components/common/JobLogLine.tsx new file mode 100644 index 0000000..5dd6f2f --- /dev/null +++ b/src/components/common/JobLogLine.tsx @@ -0,0 +1,35 @@ +import {Box, Text} from 'ink' + +import {JobLogEntry} from '@cli/types' +import {getMessageColor, getShortTime, getStageColor} from '@cli/utils/index.js' + +import {TruncatedText} from './TruncatedText.js' + +interface Props { + log: JobLogEntry + showTimestamp?: boolean + showStage?: boolean +} + +// A single line in a job log +export const JobLogLine = ({log, showTimestamp = true, showStage = true}: Props) => { + const stageColor = getStageColor(log.stage) + const messageColor = getMessageColor(log.level) + return ( + + {showTimestamp && ( + + {getShortTime(log.sentAt)} + + )} + {showStage && ( + + {log.stage} + + )} + + {log.message} + + + ) +} diff --git a/src/components/common/JobProgress.tsx b/src/components/common/JobProgress.tsx index ead7def..5bcd073 100644 --- a/src/components/common/JobProgress.tsx +++ b/src/components/common/JobProgress.tsx @@ -1,6 +1,9 @@ -import {ProgressSpinner} from '@cli/components/index.js' -import {Job} from '@cli/types/api.js' -import {getPlatformName, useJobWatching} from '@cli/utils/index.js' +import {Box, Text} from 'ink' + +import {JobLogLine, ProgressSpinner} from '@cli/components/index.js' +import {Job, JobLogEntry, LogLevel} from '@cli/types/api.js' +import {getMessageColor, getPlatformName, useJobWatching} from '@cli/utils/index.js' +import {useState} from 'react' interface Props { job: Job @@ -8,20 +11,33 @@ interface Props { onFailure?: (j: Job) => void } +// Progress bar and spinner for a job +// If the job has a warning log entry, it will show the most recent warning export const JobProgress = (props: Props) => { + const [lastWarningLog, setLastWarningLog] = useState(null) + const {progress} = useJobWatching({ isWatching: true, jobId: props.job.id, onComplete: props.onComplete, onFailure: props.onFailure, projectId: props.job.project.id, + onNewLogEntry: (logEntry: JobLogEntry) => { + if (logEntry.level == LogLevel.WARN) setLastWarningLog(logEntry) + }, }) const label = `${getPlatformName(props.job.type)} build progress...` return ( - <> + - + {lastWarningLog && ( + + WARNING + + + )} + ) } diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 47a089e..66461a8 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,4 +1,5 @@ export * from './FormTextInput.js' +export * from './JobLogLine.js' export * from './JobProgress.js' export * from './ListWithTitle.js' export * from './Markdown.js' @@ -9,4 +10,4 @@ export * from './RunWithSpinner.js' export * from './StatusTable.js' export * from './Table.js' export * from './Title.js' -export * from './TruncatedText.js' +export * from './TruncatedText.js' \ No newline at end of file From ae43434e9b6ee9790f8fca7311fd83a464f976a9 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Thu, 8 Jan 2026 17:57:00 +0000 Subject: [PATCH 09/11] Fixing bug when changing to legacy mode - have to change the export format option too --- src/utils/godot.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/utils/godot.ts b/src/utils/godot.ts index 3c22cd8..460b570 100644 --- a/src/utils/godot.ts +++ b/src/utils/godot.ts @@ -161,6 +161,10 @@ export function getGradleBuildOptionKey(majorVersion: GodotMajorVersion): string return majorVersion === 4 ? 'gradle_build/use_gradle_build' : 'custom_build/use_custom_build' } +export function getExportFormatOptionKey(majorVersion: GodotMajorVersion): string { + return majorVersion === 4 ? 'gradle_build/export_format' : 'custom_build/export_format' +} + // Tells us if Gradle build is enabled in export_presets.cfg // This uses getGodotExportPresets which uses the base preset if no config file // The base preset has Gradle enabled by default @@ -193,5 +197,10 @@ export async function setGradleBuildEnabled(value: boolean): Promise { const buildOptionKey = getGradleBuildOptionKey(majorVersion) androidPreset.options = androidPreset.options || {} androidPreset.options[buildOptionKey] = value + // If we are setting to false (legacy build), also change the export format to APK + const exportFormatOptionKey = getExportFormatOptionKey(majorVersion) + if (value === false) { + androidPreset.options[exportFormatOptionKey] = 0; // APK + } await saveExportPresets(exportPresetsPath, exportPresets) } From fdf9deaf04299269e220570535cc82c46faa92e7 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Thu, 8 Jan 2026 17:57:31 +0000 Subject: [PATCH 10/11] Fixing --follow output - need to re-apply colours --- src/components/JobFollow.tsx | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/components/JobFollow.tsx b/src/components/JobFollow.tsx index bd98259..a650477 100644 --- a/src/components/JobFollow.tsx +++ b/src/components/JobFollow.tsx @@ -1,5 +1,6 @@ import {Job, JobLogEntry, LogLevel} from '@cli/types/api.js' import {useJobWatching} from '@cli/utils/index.js' +import { useStderr, useStdout } from 'ink' interface JobFollowProps { jobId: string @@ -9,18 +10,40 @@ interface JobFollowProps { } // Outputs job logs to stdout/stderr as they come in -export const JobFollow = ({jobId, onComplete, onFailure, projectId}: JobFollowProps) => { +export const JobFollow = ({ + jobId, + onComplete, + onFailure, + projectId, +}: JobFollowProps) => { + const {stdout, write: writeOut} = useStdout(); + const {stderr, write: writeErr} = useStderr(); + + // Only emit ANSI when we're actually writing to a TTY. + const useAnsi = Boolean((stderr as any).isTTY ?? (stdout as any).isTTY); + + const yellow = useAnsi ? '\x1b[33m' : ''; + const red = useAnsi ? '\x1b[31m' : ''; + const reset = useAnsi ? '\x1b[0m' : ''; + useJobWatching({ isWatching: true, jobId, onComplete, onFailure, onNewLogEntry(logEntry: JobLogEntry) { - if (logEntry.level === LogLevel.ERROR) console.error(logEntry.message) - else console.log(logEntry.message) + const msg = logEntry.message + '\n'; + + if (logEntry.level === LogLevel.ERROR) { + writeErr(`${red}${msg}${reset}`); + } else if (logEntry.level === LogLevel.WARN) { + writeErr(`${yellow}${msg}${reset}`); + } else { + writeOut(msg); + } }, projectId, - }) + }); - return null -} + return null; +}; \ No newline at end of file From 5b61eaa0d8edeb1319c45190554cba995c820932 Mon Sep 17 00:00:00 2001 From: David Sutherland Date: Thu, 8 Jan 2026 18:41:15 +0000 Subject: [PATCH 11/11] Adding command to package.json and adding readme --- docs/util/android-build-method.md | 26 ++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 27 insertions(+) create mode 100644 docs/util/android-build-method.md diff --git a/docs/util/android-build-method.md b/docs/util/android-build-method.md new file mode 100644 index 0000000..0ac10d4 --- /dev/null +++ b/docs/util/android-build-method.md @@ -0,0 +1,26 @@ +# Command: `util android-build-method` + +## Description + +Gets and sets the Android build method in export_presets.cfg + +## Help Output + +```help +USAGE + $ shipthis util android-build-method [-l] [-g] + +FLAGS + -g, --gradle use gradle build method + -l, --legacy use legacy build method + +DESCRIPTION + Gets and sets the Android build method in export_presets.cfg + +EXAMPLES + $ shipthis util android-build-method + + $ shipthis util android-build-method --legacy + + $ shipthis util android-build-method --gradle +``` \ No newline at end of file diff --git a/package.json b/package.json index 55a3540..5f45fe5 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "exports": [ "./dist/utils/help.js", "./dist/commands/util/glass.js", + "./dist/commands/util/android-build-method.js", "./dist/commands/apple/apiKey/export.js", "./dist/commands/apple/apiKey/create.js", "./dist/commands/apple/apiKey/status.js",