diff --git a/.gitignore b/.gitignore index 1be6b26..5888e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build scratch/ # This file is generated by the runreal CLI during the test .runreal/dist/script.esm.js +.DS_Store diff --git a/deno.jsonc b/deno.jsonc index 69f9732..f3d89d4 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -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, @@ -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", @@ -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" diff --git a/deno.lock b/deno.lock index 3d5a1ac..1615f11 100644 --- a/deno.lock +++ b/deno.lock @@ -16,7 +16,6 @@ "jsr:@std/assert@1.0.12": "1.0.12", "jsr:@std/assert@^1.0.12": "1.0.12", "jsr:@std/assert@^1.0.2": "1.0.12", - "jsr:@std/assert@^1.0.8": "1.0.12", "jsr:@std/assert@~1.0.6": "1.0.12", "jsr:@std/async@^1.0.12": "1.0.12", "jsr:@std/bytes@0.221": "0.221.0", @@ -28,20 +27,16 @@ "jsr:@std/encoding@~1.0.5": "1.0.9", "jsr:@std/fmt@1": "1.0.6", "jsr:@std/fmt@1.0.6": "1.0.6", - "jsr:@std/fmt@^1.0.6": "1.0.6", "jsr:@std/fmt@~1.0.2": "1.0.6", - "jsr:@std/fs@*": "1.0.16", "jsr:@std/fs@1": "1.0.16", "jsr:@std/fs@^1.0.1": "1.0.16", "jsr:@std/fs@^1.0.16": "1.0.16", - "jsr:@std/fs@^1.0.5": "1.0.16", "jsr:@std/internal@^1.0.1": "1.0.6", - "jsr:@std/internal@^1.0.5": "1.0.6", "jsr:@std/internal@^1.0.6": "1.0.6", "jsr:@std/io@0.221": "0.221.0", + "jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/json@1": "1.0.1", "jsr:@std/jsonc@1.0.1": "1.0.1", - "jsr:@std/path@*": "1.0.8", "jsr:@std/path@1": "1.0.8", "jsr:@std/path@1.0.8": "1.0.8", "jsr:@std/path@^1.0.2": "1.0.8", @@ -52,8 +47,8 @@ "jsr:@std/testing@1.0.0": "1.0.0", "jsr:@std/testing@1.0.11": "1.0.11", "jsr:@std/text@~1.0.7": "1.0.12", - "npm:@types/node@*": "22.12.0", "npm:esbuild@0.25.2": "0.25.2", + "npm:nanoid@5.1": "5.1.5", "npm:ueblueprint@2.0.0": "2.0.0", "npm:zod-to-json-schema@3.24.5": "3.24.5_zod@3.24.2", "npm:zod@3.24.2": "3.24.2" @@ -63,7 +58,9 @@ "integrity": "f71c921cce224c13d322e5cedba4f38e8f7354c7d855c9cb22729362a53f25aa", "dependencies": [ "jsr:@cliffy/internal", - "jsr:@std/encoding@~1.0.5" + "jsr:@std/encoding@~1.0.5", + "jsr:@std/fmt@~1.0.2", + "jsr:@std/io@~0.224.9" ] }, "@cliffy/command@1.0.0-rc.7": { @@ -111,7 +108,7 @@ "jsr:@david/which", "jsr:@std/fmt@1", "jsr:@std/fs@1", - "jsr:@std/io", + "jsr:@std/io@0.221", "jsr:@std/path@1", "jsr:@std/streams@0.221" ] @@ -183,6 +180,9 @@ "jsr:@std/bytes@0.221" ] }, + "@std/io@0.224.9": { + "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" + }, "@std/json@1.0.1": { "integrity": "1f0f70737e8827f9acca086282e903677bc1bb0c8ffcd1f21bca60039563049f" }, @@ -198,7 +198,7 @@ "@std/streams@0.221.0": { "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", "dependencies": [ - "jsr:@std/io" + "jsr:@std/io@0.221" ] }, "@std/streams@1.0.9": { @@ -316,12 +316,6 @@ "@lit-labs/ssr-dom-shim" ] }, - "@types/node@22.12.0": { - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", - "dependencies": [ - "undici-types" - ] - }, "@types/trusted-types@2.0.7": { "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, @@ -377,6 +371,9 @@ "lit-html" ] }, + "nanoid@5.1.5": { + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==" + }, "parsernostrum@1.2.6": { "integrity": "sha512-Ho+y3yoqVCHRtqsKVqltsv17MgjP8Np+VIC8nd2cyEAsko5hNiZZpA6mi0krvfv8XmrS+tOpra83d4UctJtmQg==" }, @@ -387,9 +384,6 @@ "parsernostrum" ] }, - "undici-types@6.20.0": { - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, "zod-to-json-schema@3.24.5_zod@3.24.2": { "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", "dependencies": [ @@ -417,10 +411,6 @@ "https://deno.land/std@0.150.0/path/posix.ts": "c1f7afe274290ea0b51da07ee205653b2964bd74909a82deb07b69a6cc383aaa", "https://deno.land/std@0.150.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", "https://deno.land/std@0.150.0/path/win32.ts": "bd7549042e37879c68ff2f8576a25950abbfca1d696d41d82c7bca0b7e6f452c", - "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", - "https://deno.land/x/esbuild@v0.24.0/mod.js": "15b51f08198c373555700a695b6c6630a86f2c254938e81be7711eb6d4edc74e", "https://deno.land/x/globber@0.1.0/mod.ts": "971e58757909b2ef722e3dda1125aea8f5694601203ad835bdfc020f202bd5b8", "https://deno.land/x/globber@0.1.0/src/create_matcher.ts": "85be3a6d67376905521aed9da51db756d1ee747ebd0d52b88fc7b78a6831a393", "https://deno.land/x/globber@0.1.0/src/deps.ts": "179ba170213f7a35b7b794c409e7ca523da58644139c053721f93575dcbe616e", @@ -449,6 +439,7 @@ }, "workspace": { "dependencies": [ + "jsr:@cliffy/ansi@1.0.0-rc.7", "jsr:@cliffy/command@1.0.0-rc.7", "jsr:@cliffy/testing@1.0.0-rc.7", "jsr:@david/dax@0.42.0", @@ -463,6 +454,7 @@ "jsr:@std/streams@1.0.9", "jsr:@std/testing@1.0.11", "npm:esbuild@0.25.2", + "npm:nanoid@5.1", "npm:ueblueprint@2.0.0", "npm:zod-to-json-schema@3.24.5", "npm:zod@3.24.2" diff --git a/src/commands/auth.ts b/src/commands/auth.ts new file mode 100644 index 0000000..d99bb0b --- /dev/null +++ b/src/commands/auth.ts @@ -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() + .description('auth') + .arguments(' [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) +} diff --git a/src/index.ts b/src/index.ts index dccffd8..bf7b8fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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') @@ -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) diff --git a/src/lib/preferences.ts b/src/lib/preferences.ts new file mode 100644 index 0000000..f8b5651 --- /dev/null +++ b/src/lib/preferences.ts @@ -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 => { + 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 => { + 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, +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index e3e5f5b..17d3cd9 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -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'), +}) diff --git a/src/lib/types.ts b/src/lib/types.ts index dc9b7cb..c8c31c8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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 ? Options @@ -29,6 +29,8 @@ export type RunrealConfig = z.infer & InternalRunrealConfig export type UserRunrealConfig = z.infer +export type UserRunrealPreferences = z.infer + export interface UeDepsManifestData { Name: string Hash: string