Skip to content

Commit 449eca8

Browse files
authored
feat: add runreal auth (#54)
* feat: add cli auth * chore: fix types * feat: cli auth with polling
1 parent ef98cb0 commit 449eca8

File tree

8 files changed

+191
-31
lines changed

8 files changed

+191
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ build
22
scratch/
33
# This file is generated by the runreal CLI during the test
44
.runreal/dist/script.esm.js
5+
.DS_Store

deno.jsonc

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,29 @@
1010
"generate-schema": "deno run -A src/generate-schema.ts"
1111
},
1212
"lint": {
13-
"include": ["src/", "tests/"],
13+
"include": [
14+
"src/",
15+
"tests/"
16+
],
1417
"rules": {
15-
"tags": ["recommended"],
16-
"include": ["ban-untagged-todo"],
17-
"exclude": ["no-unused-vars", "no-explicit-any"]
18+
"tags": [
19+
"recommended"
20+
],
21+
"include": [
22+
"ban-untagged-todo"
23+
],
24+
"exclude": [
25+
"no-unused-vars",
26+
"no-explicit-any"
27+
]
1828
}
1929
},
2030
"fmt": {
21-
"include": ["src/", "tests/"],
31+
"include": [
32+
"src/",
33+
"tests/",
34+
"deno.jsonc"
35+
],
2236
"useTabs": true,
2337
"lineWidth": 120,
2438
"indentWidth": 2,
@@ -27,6 +41,7 @@
2741
"semiColons": false
2842
},
2943
"imports": {
44+
"@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.7",
3045
"@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7",
3146
"@cliffy/testing": "jsr:@cliffy/testing@1.0.0-rc.7",
3247
"@david/dax": "jsr:@david/dax@0.42.0",
@@ -41,10 +56,11 @@
4156
"@std/streams": "jsr:@std/streams@1.0.9",
4257
"@std/testing": "jsr:@std/testing@1.0.11",
4358
"esbuild": "npm:esbuild@0.25.2",
44-
"ueblueprint":"npm:ueblueprint@2.0.0",
59+
"ueblueprint": "npm:ueblueprint@2.0.0",
4560
"zod": "npm:zod@3.24.2",
4661
"zod-to-json-schema": "npm:zod-to-json-schema@3.24.5",
4762
"ndjson": "https://deno.land/x/ndjson@1.1.0/mod.ts",
63+
"nanoid": "npm:nanoid@5.1",
4864
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts",
4965
"xml2js": "https://deno.land/x/xml2js@1.0.0/mod.ts",
5066
"globber": "https://deno.land/x/globber@0.1.0/mod.ts"

deno.lock

Lines changed: 15 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/auth.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Command } from '@cliffy/command'
2+
import { customAlphabet } from 'nanoid'
3+
import { colors } from '@cliffy/ansi/colors'
4+
import type { GlobalOptions } from '../lib/types.ts'
5+
import { preferences } from '../lib/preferences.ts'
6+
7+
// base58 uppercase characters
8+
const idgen = customAlphabet('123456789ABCDEFGHJKMNPQRSTUVWXYZ', 6)
9+
10+
const RUNREAL_API_ENDPOINT = Deno.env.get('RUNREAL_API_ENDPOINT') || 'https://api.dashboard.runreal.dev/v1'
11+
const RUNREAL_AUTH_ENDPOINT = Deno.env.get('RUNREAL_AUTH_ENDPOINT') || 'https://auth.runreal.dev'
12+
13+
export const auth = new Command<GlobalOptions>()
14+
.description('auth')
15+
.arguments('<command> [args...]')
16+
.stopEarly()
17+
.action(async (options, command, ...args) => {
18+
if (command === 'login') {
19+
const code = idgen(6)
20+
const hostname = Deno.hostname().replace(/[^a-z0-9A-Z-_\.]/, '')
21+
22+
const link = `${RUNREAL_AUTH_ENDPOINT}/auth/cli?hostname=${hostname}&code=${code}`
23+
24+
console.log(`Login code: ${colors.bold.green(code)}`)
25+
console.log(`Please open ${colors.bold.cyan(link)} to authorize.`)
26+
27+
let count = 0
28+
const interval = setInterval(async () => {
29+
if (count > 20) {
30+
clearInterval(interval)
31+
console.log(colors.red('Authentication timed out.'))
32+
shutdown()
33+
return
34+
}
35+
36+
await fetch(`${RUNREAL_API_ENDPOINT}/cli`, {
37+
method: 'POST',
38+
body: JSON.stringify({
39+
hostname,
40+
code,
41+
}),
42+
headers: {
43+
'Content-Type': 'application/json',
44+
},
45+
}).then(async (res) => {
46+
if (res.status === 200) {
47+
clearInterval(interval)
48+
const body = await res.json()
49+
50+
await preferences.set({
51+
accessToken: body.token,
52+
})
53+
console.log(colors.bold.cyan('Authenticated successfully!'))
54+
shutdown()
55+
}
56+
}).catch((err) => {
57+
console.log(err)
58+
// ignore error
59+
})
60+
count++
61+
}, 5000)
62+
63+
return
64+
}
65+
66+
if (command === 'logout') {
67+
const prefs = await preferences.get()
68+
if (prefs.accessToken) {
69+
delete prefs.accessToken
70+
await preferences.set(prefs)
71+
console.log(colors.bold.cyan('Logged out successfully!'))
72+
} else {
73+
console.log(colors.red('You are not logged in.'))
74+
}
75+
return
76+
}
77+
78+
throw new Error('Invalid command. Use "login" or "logout".')
79+
})
80+
81+
function shutdown() {
82+
setTimeout(() => {
83+
Deno.exit(0)
84+
}, 500)
85+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { cmd } from './cmd.ts'
1414
import { script } from './commands/script.ts'
1515
import { runpython } from './commands/runpython.ts'
1616
import { uasset } from './commands/uasset/index.ts'
17+
import { auth } from './commands/auth.ts'
1718

1819
await cmd
1920
.name('runreal')
@@ -33,6 +34,7 @@ await cmd
3334
.command('buildgraph', buildgraph)
3435
.command('workflow', workflow)
3536
.command('script', script)
37+
.command('auth', auth)
3638
.command('runpython', runpython)
3739
.command('uasset', uasset)
3840
.parse(Deno.args)

src/lib/preferences.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { UserRunrealPreferencesSchema } from './schema.ts'
2+
import { UserRunrealPreferences } from './types.ts'
3+
4+
const homeDir = Deno.env.get('HOME')
5+
const preferencesPath = `${homeDir}/.runreal/`
6+
const preferencesFile = 'preferences.json'
7+
const preferencesFullPath = `${preferencesPath}${preferencesFile}`
8+
9+
const get = async (): Promise<UserRunrealPreferences> => {
10+
let prefs: UserRunrealPreferences = {}
11+
try {
12+
const fileInfo = await Deno.stat(preferencesFullPath)
13+
if (!fileInfo.isFile) {
14+
return prefs
15+
}
16+
const file = await Deno.readTextFile(preferencesFullPath)
17+
const parsed = JSON.parse(file)
18+
try {
19+
prefs = UserRunrealPreferencesSchema.parse(parsed)
20+
} catch (e) {
21+
console.error('Invalid preferences file:', e)
22+
return prefs
23+
}
24+
} catch (e) {
25+
console.error('Error reading preferences file:', e)
26+
if (e instanceof Deno.errors.NotFound) {
27+
return prefs
28+
}
29+
throw e
30+
}
31+
32+
return prefs
33+
}
34+
35+
const set = async (prefs: UserRunrealPreferences): Promise<UserRunrealPreferences> => {
36+
try {
37+
const fileInfo = await Deno.stat(preferencesFullPath)
38+
if (!fileInfo.isFile) {
39+
await Deno.mkdir(preferencesPath, { recursive: true })
40+
}
41+
} catch (e) {
42+
if (e instanceof Deno.errors.NotFound) {
43+
await Deno.mkdir(preferencesPath, { recursive: true })
44+
} else {
45+
throw e
46+
}
47+
}
48+
49+
const data = JSON.stringify(prefs, null, 2)
50+
await Deno.writeTextFile(preferencesFullPath, data)
51+
52+
return prefs
53+
}
54+
55+
export const preferences = {
56+
get,
57+
set,
58+
}

src/lib/schema.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@ export const ConfigSchema = z.object({
6161

6262
export const RunrealConfigSchema = ConfigSchema.merge(InternalSchema)
6363

64-
// Depraecated 😭
64+
// Deprecated
6565
export const UserRunrealConfigSchema = ConfigSchema.deepPartial()
66+
67+
export const UserRunrealPreferencesSchema = z.object({
68+
accessToken: z.string().optional().describe('RUNREAL access token'),
69+
})

src/lib/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { DebugConfigOptions } from '../commands/debug/debug-config.ts'
88
import type { SetupOptions } from '../commands/engine/setup.ts'
99
import type { InstallOptions } from '../commands/engine/install.ts'
1010
import type { UpdateOptions } from '../commands/engine/update.ts'
11-
import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema } from './schema.ts'
11+
import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema, UserRunrealPreferencesSchema } from './schema.ts'
1212
import type { Type } from '@cliffy/command'
1313

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

3030
export type UserRunrealConfig = z.infer<typeof UserRunrealConfigSchema>
3131

32+
export type UserRunrealPreferences = z.infer<typeof UserRunrealPreferencesSchema>
33+
3234
export interface UeDepsManifestData {
3335
Name: string
3436
Hash: string

0 commit comments

Comments
 (0)