Skip to content

Commit 46cd3a2

Browse files
committed
ship command refactor and prototype go command showing the qr code
1 parent c562fc9 commit 46cd3a2

15 files changed

Lines changed: 418 additions & 182 deletions

File tree

src/api/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ export async function getJob(jobId: string, projectId: string): Promise<Job> {
140140
return castJobDates(data)
141141
}
142142

143+
const MAX_RETRIES = 3
144+
const RETRY_DELAY_MS = 5000
145+
146+
// Returns the builds for a job, retrying if none are found
147+
// The retry is because of possible race condition when a job completes
148+
export async function getJobBuildsRetry(jobId: string, projectId: string, retries = MAX_RETRIES): Promise<Build[]> {
149+
let job: Job | null = null
150+
151+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
152+
job = await getJob(jobId, projectId)
153+
if (job.builds && job.builds.length > 0) break
154+
if (attempt < MAX_RETRIES) await new Promise((res) => setTimeout(res, RETRY_DELAY_MS))
155+
}
156+
157+
if (!job?.builds || job.builds.length === 0) throw new Error('No builds found for this job after multiple attempts')
158+
159+
return job.builds
160+
}
161+
143162
// Returns a url with an OTP - when visited it authenticates the user
144163
export async function getSingleUseUrl(destination: string) {
145164
// Call the API to generate an OTP

src/commands/game/go.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {BaseGameCommand} from '@cli/baseCommands/index.js'
2+
import {Go} from '@cli/components/Go.js'
3+
import {CommandGame} from '@cli/components/index.js'
4+
5+
import {render} from 'ink'
6+
7+
export default class GameGo extends BaseGameCommand<typeof GameGo> {
8+
static override args = {}
9+
static override description = 'Preview your game in the ShipThis Go app.'
10+
static override examples = ['<%= config.bin %> <%= command.id %>']
11+
static override flags = {
12+
...BaseGameCommand.flags,
13+
}
14+
15+
public async run(): Promise<void> {
16+
await this.ensureWeAreInAProjectDir()
17+
const gameId = this.getGameId()
18+
if (!gameId) {
19+
this.error('No game ID found')
20+
}
21+
22+
const handleComplete = () => process.exit(0)
23+
const handleError = (error: any) => {
24+
this.error(`Error generating go build: ${error}`)
25+
}
26+
27+
render(
28+
<CommandGame command={this}>
29+
<Go onComplete={handleComplete} onError={handleError} />
30+
</CommandGame>,
31+
)
32+
}
33+
}

src/commands/game/ship.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Flags} from '@oclif/core'
22
import {render} from 'ink'
33

4-
import {downloadBuildById, getJob} from '@cli/api/index.js'
4+
import {downloadBuildById, getJobBuildsRetry} from '@cli/api/index.js'
55
import {BaseGameCommand} from '@cli/baseCommands/baseGameCommand.js'
66
import {CommandGame, Ship} from '@cli/components/index.js'
77
import {Job} from '@cli/types/api.js'
@@ -62,26 +62,13 @@ export default class GameShip extends BaseGameCommand<typeof GameShip> {
6262
this.error('No game ID found')
6363
}
6464

65-
const MAX_RETRIES = 3
66-
const RETRY_DELAY_MS = 5000
67-
68-
const handleComplete = async ([originalJob]: Job[]) => {
65+
const handleComplete = async ([job]: Job[]) => {
6966
if (!this.flags.download && !this.flags.downloadAPK) return process.exit(0)
70-
71-
let job: Job | null = null
72-
73-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
74-
job = await getJob(originalJob.id, gameId)
75-
if (job.builds && job.builds.length > 0) break
76-
if (attempt < MAX_RETRIES) await new Promise((res) => setTimeout(res, RETRY_DELAY_MS))
77-
}
78-
79-
if (!job?.builds || job.builds.length === 0) this.error('No builds found for this job after multiple attempts')
80-
67+
// Use a retry mechanism to get the builds, as they may not be immediately available
68+
const builds = await getJobBuildsRetry(job.id, gameId)
8169
const {platform} = this.flags
8270
const type = platform === 'android' ? (this.flags.downloadAPK ? 'APK' : 'AAB') : 'IPA'
83-
84-
const build = job.builds.find((b) => b.buildType === type)
71+
const build = builds.find((b) => b.buildType === type)
8572
if (!build) this.error(`No build found for type ${type}`)
8673

8774
const filename = this.flags.download || this.flags.downloadAPK

src/components/Go.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {useContext, useState} from 'react'
2+
3+
import {getShortUUID, useProjectJobListener, useStartShipOnMount} from '@cli/utils/index.js'
4+
import {getJobBuildsRetry} from '@cli/api/index.js'
5+
6+
import {CommandContext, GameContext, JobProgress, QRCodeTerminal} from './index.js'
7+
8+
interface Props {
9+
onComplete: () => void
10+
onError: (error: any) => void
11+
}
12+
13+
export const Go = ({onComplete, onError}: Props): JSX.Element| null => {
14+
const {command} = useContext(CommandContext)
15+
const {gameId} = useContext(GameContext)
16+
if (!command || !gameId) return null
17+
return <GoCommand command={command} gameId={gameId} onComplete={onComplete} onError={onError} />
18+
}
19+
20+
interface GoCommandProps extends Props {
21+
command: any
22+
gameId: string
23+
}
24+
25+
const GoCommand = ({command, gameId, onComplete, onError}: GoCommandProps): JSX.Element | null=> {
26+
const flags = {follow: false, platform: 'go'}
27+
28+
const {jobs: startedJobs} = useStartShipOnMount(command, flags, onError)
29+
30+
const handleJobCompleted = async (job: any) => {
31+
const [goBuild] = await getJobBuildsRetry(job.id, command.getGameId())
32+
setQRCodeData(getShortUUID(goBuild.id))
33+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
34+
await sleep(500)
35+
onComplete()
36+
}
37+
38+
const handleJobFailed = (job: any) => {
39+
onError(new Error(`Go job failed: ${job.id}`))
40+
}
41+
42+
const {jobsById} = useProjectJobListener({
43+
projectId: gameId,
44+
onJobCompleted: handleJobCompleted,
45+
onJobFailed: handleJobFailed,
46+
})
47+
48+
const [qrCodeData, setQRCodeData] = useState<string | null>(null)
49+
50+
if (qrCodeData) {
51+
return <QRCodeTerminal data={qrCodeData} />
52+
}
53+
54+
if (startedJobs && startedJobs?.length > 0) {
55+
return <JobProgress job={startedJobs[0]} />
56+
}
57+
58+
return null
59+
}

src/components/Ship.tsx

Lines changed: 0 additions & 160 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {Dispatch, SetStateAction} from 'react'
2+
import {Text} from 'ink'
3+
import open from 'open'
4+
5+
import {getShortAuthRequiredUrl} from '@cli/api/index.js'
6+
import {Job} from '@cli/types/api.js'
7+
import {useSafeInput} from '@cli/utils/index.js'
8+
9+
interface KeyboardShortcutsProps {
10+
onToggleJobLogs: Dispatch<SetStateAction<boolean>>
11+
gameId?: string
12+
jobs?: Job[] | null
13+
}
14+
15+
export const KeyboardShortcuts = ({onToggleJobLogs, gameId, jobs}: KeyboardShortcutsProps) => {
16+
useSafeInput(async (input) => {
17+
if (!gameId) return
18+
const i = input.toLowerCase()
19+
switch (i) {
20+
case 'l': {
21+
onToggleJobLogs((prev) => !prev)
22+
break
23+
}
24+
25+
case 'b': {
26+
const dashUrl = jobs?.length === 1 ? `/games/${gameId}/job/${jobs[0].id}` : `/games/${gameId}`
27+
const url = await getShortAuthRequiredUrl(dashUrl)
28+
await open(url)
29+
break
30+
}
31+
}
32+
})
33+
34+
return (
35+
<>
36+
<Text>Press L to show and hide the job logs.</Text>
37+
<Text>Press B to open the ShipThis dashboard in your browser.</Text>
38+
</>
39+
)
40+
}

0 commit comments

Comments
 (0)