Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build
scratch/
# This file is generated by the runreal CLI during the test
.runreal/dist/script.esm.js
.DS_Store
28 changes: 22 additions & 6 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,29 @@
"generate-schema": "deno run -A src/generate-schema.ts"
},
"lint": {
"include": ["src/", "tests/"],
"include": [
"src/",
"tests/"
],
"rules": {
"tags": ["recommended"],
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars", "no-explicit-any"]
"tags": [
"recommended"
],
"include": [
"ban-untagged-todo"
],
"exclude": [
"no-unused-vars",
"no-explicit-any"
]
}
},
"fmt": {
"include": ["src/", "tests/"],
"include": [
"src/",
"tests/",
"deno.jsonc"
],
"useTabs": true,
"lineWidth": 120,
"indentWidth": 2,
Expand All @@ -27,6 +41,7 @@
"semiColons": false
},
"imports": {
"@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.7",
"@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7",
"@cliffy/testing": "jsr:@cliffy/testing@1.0.0-rc.7",
"@david/dax": "jsr:@david/dax@0.42.0",
Expand All @@ -41,10 +56,11 @@
"@std/streams": "jsr:@std/streams@1.0.9",
"@std/testing": "jsr:@std/testing@1.0.11",
"esbuild": "npm:esbuild@0.25.2",
"ueblueprint":"npm:ueblueprint@2.0.0",
"ueblueprint": "npm:ueblueprint@2.0.0",
"zod": "npm:zod@3.24.2",
"zod-to-json-schema": "npm:zod-to-json-schema@3.24.5",
"ndjson": "https://deno.land/x/ndjson@1.1.0/mod.ts",
"nanoid": "npm:nanoid@5.1",
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts",
"xml2js": "https://deno.land/x/xml2js@1.0.0/mod.ts",
"globber": "https://deno.land/x/globber@0.1.0/mod.ts"
Expand Down
38 changes: 15 additions & 23 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Command } from '@cliffy/command'
import { customAlphabet } from 'nanoid'
import { colors } from '@cliffy/ansi/colors'
import type { GlobalOptions } from '../lib/types.ts'
import { preferences } from '../lib/preferences.ts'

// base58 uppercase characters
const idgen = customAlphabet('123456789ABCDEFGHJKMNPQRSTUVWXYZ', 6)

const RUNREAL_API_ENDPOINT = Deno.env.get('RUNREAL_API_ENDPOINT') || 'https://api.dashboard.runreal.dev/v1'
const RUNREAL_AUTH_ENDPOINT = Deno.env.get('RUNREAL_AUTH_ENDPOINT') || 'https://auth.runreal.dev'

export const auth = new Command<GlobalOptions>()
.description('auth')
.arguments('<command> [args...]')
.stopEarly()
.action(async (options, command, ...args) => {
if (command === 'login') {
const code = idgen(6)
const hostname = Deno.hostname().replace(/[^a-z0-9A-Z-_\.]/, '')

const link = `${RUNREAL_AUTH_ENDPOINT}/auth/cli?hostname=${hostname}&code=${code}`

console.log(`Login code: ${colors.bold.green(code)}`)
console.log(`Please open ${colors.bold.cyan(link)} to authorize.`)

let count = 0
const interval = setInterval(async () => {
if (count > 20) {
clearInterval(interval)
console.log(colors.red('Authentication timed out.'))
shutdown()
return
}

await fetch(`${RUNREAL_API_ENDPOINT}/cli`, {
method: 'POST',
body: JSON.stringify({
hostname,
code,
}),
headers: {
'Content-Type': 'application/json',
},
}).then(async (res) => {
if (res.status === 200) {
clearInterval(interval)
const body = await res.json()

await preferences.set({
accessToken: body.token,
})
console.log(colors.bold.cyan('Authenticated successfully!'))
shutdown()
}
}).catch((err) => {
console.log(err)
// ignore error
})
count++
}, 5000)

return
}

if (command === 'logout') {
const prefs = await preferences.get()
if (prefs.accessToken) {
delete prefs.accessToken
await preferences.set(prefs)
console.log(colors.bold.cyan('Logged out successfully!'))
} else {
console.log(colors.red('You are not logged in.'))
}
return
}

throw new Error('Invalid command. Use "login" or "logout".')
})

function shutdown() {
setTimeout(() => {
Deno.exit(0)
}, 500)
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { cmd } from './cmd.ts'
import { script } from './commands/script.ts'
import { runpython } from './commands/runpython.ts'
import { uasset } from './commands/uasset/index.ts'
import { auth } from './commands/auth.ts'

await cmd
.name('runreal')
Expand All @@ -33,6 +34,7 @@ await cmd
.command('buildgraph', buildgraph)
.command('workflow', workflow)
.command('script', script)
.command('auth', auth)
.command('runpython', runpython)
.command('uasset', uasset)
.parse(Deno.args)
58 changes: 58 additions & 0 deletions src/lib/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { UserRunrealPreferencesSchema } from './schema.ts'
import { UserRunrealPreferences } from './types.ts'

const homeDir = Deno.env.get('HOME')
const preferencesPath = `${homeDir}/.runreal/`
const preferencesFile = 'preferences.json'
const preferencesFullPath = `${preferencesPath}${preferencesFile}`

const get = async (): Promise<UserRunrealPreferences> => {
let prefs: UserRunrealPreferences = {}
try {
const fileInfo = await Deno.stat(preferencesFullPath)
if (!fileInfo.isFile) {
return prefs
}
const file = await Deno.readTextFile(preferencesFullPath)
const parsed = JSON.parse(file)
try {
prefs = UserRunrealPreferencesSchema.parse(parsed)
} catch (e) {
console.error('Invalid preferences file:', e)
return prefs
}
} catch (e) {
console.error('Error reading preferences file:', e)
if (e instanceof Deno.errors.NotFound) {
return prefs
}
throw e
}

return prefs
}

const set = async (prefs: UserRunrealPreferences): Promise<UserRunrealPreferences> => {
try {
const fileInfo = await Deno.stat(preferencesFullPath)
if (!fileInfo.isFile) {
await Deno.mkdir(preferencesPath, { recursive: true })
}
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
await Deno.mkdir(preferencesPath, { recursive: true })
} else {
throw e
}
}

const data = JSON.stringify(prefs, null, 2)
await Deno.writeTextFile(preferencesFullPath, data)

return prefs
}

export const preferences = {
get,
set,
}
6 changes: 5 additions & 1 deletion src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,9 @@ export const ConfigSchema = z.object({

export const RunrealConfigSchema = ConfigSchema.merge(InternalSchema)

// Depraecated 😭
// Deprecated
export const UserRunrealConfigSchema = ConfigSchema.deepPartial()

export const UserRunrealPreferencesSchema = z.object({
accessToken: z.string().optional().describe('RUNREAL access token'),
})
4 changes: 3 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { DebugConfigOptions } from '../commands/debug/debug-config.ts'
import type { SetupOptions } from '../commands/engine/setup.ts'
import type { InstallOptions } from '../commands/engine/install.ts'
import type { UpdateOptions } from '../commands/engine/update.ts'
import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema } from './schema.ts'
import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema, UserRunrealPreferencesSchema } from './schema.ts'
import type { Type } from '@cliffy/command'

export type GlobalOptions = typeof cmd extends Command<void, void, void, [], infer Options> ? Options
Expand All @@ -29,6 +29,8 @@ export type RunrealConfig = z.infer<typeof ConfigSchema> & InternalRunrealConfig

export type UserRunrealConfig = z.infer<typeof UserRunrealConfigSchema>

export type UserRunrealPreferences = z.infer<typeof UserRunrealPreferencesSchema>

export interface UeDepsManifestData {
Name: string
Hash: string
Expand Down