mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-26 12:32:25 +03:00
✨ feat: Add auth flow (#146)
This adds a simple way to incorporate a centralized authentication flow. The idea is to have the user, API and SSH (for machine authentication) all in one place using `openauthjs` + `SST` We also have a database now :) > We are using InstantDB as it allows us to authenticate a use with just the email. Plus it is super simple simple to use _of course after the initial fumbles trying to design the db and relationships_
This commit is contained in:
@@ -50,3 +50,6 @@ id_*
|
||||
|
||||
#Rust
|
||||
target
|
||||
|
||||
tmp
|
||||
.partykit
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-2
@@ -1,7 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"@nestri/eslint-config/qwik.js",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:qwik/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
@@ -12,5 +19,24 @@ module.exports = {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"prefer-spread": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"@typescript-eslint/consistent-type-imports": "warn",
|
||||
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||
},
|
||||
};
|
||||
@@ -35,11 +35,10 @@
|
||||
"@builder.io/qwik-city": "^1.8.0",
|
||||
"@builder.io/qwik-react": "0.5.0",
|
||||
"@modular-forms/qwik": "^0.27.0",
|
||||
"@nestri/eslint-config": "*",
|
||||
"@nestri/input": "*",
|
||||
"@nestri/libmoq": "*",
|
||||
"@nestri/typescript-config": "*",
|
||||
"@nestri/ui": "*",
|
||||
"@openauthjs/openauth": "^0.2.6",
|
||||
"@types/eslint": "8.56.10",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/node": "^22.5.1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// module.exports = require("@nestri/ui/postcss.config");
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { component$ } from "@builder.io/qwik"
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<div class="font-title">
|
||||
Device
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { $, component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
|
||||
function getHashParams(url: URL) {
|
||||
const urlString = url.toString()
|
||||
const hash = urlString.substring(urlString.indexOf('#') + 1); // Extract the part after the #
|
||||
console.log("url", hash)
|
||||
const params = new URLSearchParams(hash);
|
||||
const paramsObj = {} as any;
|
||||
for (const [key, value] of params.entries()) {
|
||||
paramsObj[key] = decodeURIComponent(value);
|
||||
}
|
||||
console.log(paramsObj)
|
||||
return paramsObj;
|
||||
}
|
||||
|
||||
function removeURLParams() {
|
||||
const newURL = window.location.origin + window.location.pathname; // Just origin and path
|
||||
window.location.replace(newURL);
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
|
||||
const login = $(async () => {
|
||||
const client = createClient({
|
||||
clientID: "www",
|
||||
issuer: "https://auth.lauryn.dev.nestri.io"
|
||||
})
|
||||
|
||||
const { url } = await client.authorize("http://localhost:5173/login", "token", { pkce: true })
|
||||
window.location.href = url
|
||||
})
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(async () => {
|
||||
const urlObj = new URL(window.location.href);
|
||||
const params = getHashParams(urlObj)
|
||||
if (params.access_token && params.refresh_token) {
|
||||
|
||||
localStorage.setItem("access_token", params.access_token)
|
||||
localStorage.setItem("refresh_token", params.refresh_token)
|
||||
removeURLParams()
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
return (
|
||||
<div class="h-screen w-screen flex justify-center items-center">
|
||||
<button class="px-2 py-1 font-title text-lg bg-gray-400 rounded-lg" onClick$={login}>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -1,119 +0,0 @@
|
||||
import * as v from "valibot"
|
||||
//FIXME: Make sure this works
|
||||
// import { Broadcast } from "./tester";
|
||||
import { cn } from "@nestri/ui/design";
|
||||
import { routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { component$, $, useSignal } from "@builder.io/qwik";
|
||||
import { MotionComponent, transition, TitleSection, Button } from "@nestri/ui/react";
|
||||
import { type InitialValues, type SubmitHandler, useForm, valiForm$ } from "@modular-forms/qwik"
|
||||
|
||||
const Schema = v.object({
|
||||
url: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(10, "Please input a valid url"),
|
||||
v.url("Please input a valid url"),
|
||||
)
|
||||
}, "Please fill in all the fields correctly.")
|
||||
|
||||
type Form = v.InferInput<typeof Schema>;
|
||||
|
||||
export const useFormLoader = routeLoader$<InitialValues<Form>>(async () => {
|
||||
return {
|
||||
url: ""
|
||||
}
|
||||
})
|
||||
|
||||
const generateRandomWord = (length: number) => {
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
return Array.from({ length }, () => characters[Math.floor(Math.random() * characters.length)]).join('');
|
||||
};
|
||||
|
||||
export default component$(() => {
|
||||
const broadcasterOk = useSignal<boolean | undefined>();
|
||||
const [state, { Form, Field }] = useForm<Form>({
|
||||
loader: useFormLoader(),
|
||||
validate: valiForm$(Schema)
|
||||
});
|
||||
|
||||
const handleSubmit = $<SubmitHandler<Form>>(async (values) => {
|
||||
const randomNamespace = generateRandomWord(6);
|
||||
// const sub = await Broadcast.init({ url: values.url, fingerprint: undefined, namespace: randomNamespace })
|
||||
|
||||
// setTimeout(() => {
|
||||
// broadcasterOk.value = sub.isSubscribed()
|
||||
// }, 1000);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection client:load title="MoQ Checker" description="Test the connection to your Media-Over-Quic relay!" />
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 100 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={transition}
|
||||
client:load
|
||||
class="flex items-center justify-center w-full"
|
||||
as="div"
|
||||
>
|
||||
<section class="w-full flex flex-col gap-4 justify-center items-center">
|
||||
<Form onSubmit$={handleSubmit} class="w-full max-w-xl flex px-3 gap-2">
|
||||
<Field name="url">
|
||||
{(field, props) => {
|
||||
return (
|
||||
<div class="w-full flex flex-col gap-2">
|
||||
<div class="bg-gray-200 dark:bg-gray-800 flex rounded-lg w-full relative h-10 flex-none border focus-within:bg-gray-300/70 dark:focus-within:bg-gray-700/70 border-gray-300 dark:border-gray-700 ">
|
||||
<input type="url" class={cn("w-full relative h-full bg-transparent rounded-lg p-3 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-400 dark:focus-within:ring-gray-600 focus-within:ring-offset-2 focus-visible:outline-none focus-within:ring-offset-gray-100 dark:focus-within:ring-offset-gray-900 placeholder:text-gray-500/70", typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-green-500", typeof broadcasterOk.value !== "undefined" && (broadcasterOk.value == false) && "ring-2 ring-offset-2 ring-offset-gray-100 dark:ring-offset-gray-900 ring-red-500")} placeholder="https://relay.domain.com" {...props} />
|
||||
</div>
|
||||
{field.error && (<p class='text-[0.8rem] font-medium text-danger-600 dark:text-danger-500' >{field.error}</p>)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Field>
|
||||
|
||||
{/* <button class={cn(buttonVariants.solid({ size: "md", intent: "neutral" }), "w-max space-y-0 relative")} style={{ height: 40, marginTop: 0 }} type="submit" >
|
||||
Check
|
||||
</button> */}
|
||||
<Button.Root
|
||||
disabled={state.submitting}
|
||||
isLoading={state.submitting}
|
||||
// setIsLoading={setIsLoading}
|
||||
client:load
|
||||
//@ts-ignore
|
||||
type="submit"
|
||||
style={{ height: 40, marginTop: 0 }}
|
||||
intent="neutral"
|
||||
size="md"
|
||||
class="w-max space-y-0 relative">
|
||||
{/* <Button.Icon
|
||||
isLoading={isLoading.value}
|
||||
client:load>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
|
||||
<g fill="currentColor">
|
||||
<path d="M.329 10.333A8.01 8.01 0 0 0 7.99 16C12.414 16 16 12.418 16 8s-3.586-8-8.009-8A8.006 8.006 0 0 0 0 7.468l.003.006l4.304 1.769A2.2 2.2 0 0 1 5.62 8.88l1.96-2.844l-.001-.04a3.046 3.046 0 0 1 3.042-3.043a3.046 3.046 0 0 1 3.042 3.043a3.047 3.047 0 0 1-3.111 3.044l-2.804 2a2.223 2.223 0 0 1-3.075 2.11a2.22 2.22 0 0 1-1.312-1.568L.33 10.333Z" /><path d="M4.868 12.683a1.715 1.715 0 0 0 1.318-3.165a1.7 1.7 0 0 0-1.263-.02l1.023.424a1.261 1.261 0 1 1-.97 2.33l-.99-.41a1.7 1.7 0 0 0 .882.84Zm3.726-6.687a2.03 2.03 0 0 0 2.027 2.029a2.03 2.03 0 0 0 2.027-2.029a2.03 2.03 0 0 0-2.027-2.027a2.03 2.03 0 0 0-2.027 2.027m2.03-1.527a1.524 1.524 0 1 1-.002 3.048a1.524 1.524 0 0 1 .002-3.048" />
|
||||
</g>
|
||||
</svg>
|
||||
</Button.Icon> */}
|
||||
<Button.Label
|
||||
loadingText="Checking..."
|
||||
class="text-ellipsis whitespace-nowrap"
|
||||
isLoading={state.submitting}>
|
||||
Check
|
||||
</Button.Label>
|
||||
<div class="w-[8%]" />
|
||||
</Button.Root>
|
||||
</Form>
|
||||
{typeof broadcasterOk.value !== "undefined" && broadcasterOk.value == true ? (
|
||||
<span class="w-full text-green-500 max-w-xl flex space-y-6 px-3 gap-2">
|
||||
Your relay is doing okay
|
||||
</span>
|
||||
) : typeof broadcasterOk.value !== "undefined" && (
|
||||
<span class="w-full text-red-500 max-w-xl flex space-y-6 px-3 gap-2">
|
||||
Your relay has an issue
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
</MotionComponent>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1,208 +0,0 @@
|
||||
// import type { Connection, SubscribeRecv } from "@nestri/libmoq/transport"
|
||||
// import { asError } from "@nestri/moq/common/error"
|
||||
// import { Client } from "@nestri/moq/transport/client"
|
||||
// import * as Catalog from "@nestri/moq/media/catalog"
|
||||
// import { type GroupWriter } from "@nestri/moq/transport/objects"
|
||||
|
||||
// export interface BroadcastConfig {
|
||||
// namespace: string
|
||||
// connection: Connection
|
||||
// }
|
||||
// export interface BroadcasterConfig {
|
||||
// url: string
|
||||
// namespace: string
|
||||
// fingerprint?: string // URL to fetch TLS certificate fingerprint
|
||||
// }
|
||||
|
||||
// export interface BroadcastConfigTrack {
|
||||
// input: string
|
||||
// bitrate: number
|
||||
// }
|
||||
|
||||
// export class Broadcast {
|
||||
// stream: GroupWriter | null
|
||||
// subscriber: SubscribeRecv | null
|
||||
// subscribed: boolean;
|
||||
|
||||
|
||||
// readonly config: BroadcastConfig
|
||||
// readonly catalog: Catalog.Root
|
||||
// readonly connection: Connection
|
||||
// readonly namespace: string
|
||||
|
||||
// #running: Promise<void>
|
||||
|
||||
// constructor(config: BroadcastConfig) {
|
||||
// this.subscribed = false
|
||||
// this.namespace = config.namespace
|
||||
// this.connection = config.connection
|
||||
// this.config = config
|
||||
// //Arbitrary values, just to keep TypeScript happy :)
|
||||
// this.catalog = {
|
||||
// version: 1,
|
||||
// streamingFormat: 1,
|
||||
// streamingFormatVersion: "0.2",
|
||||
// supportsDeltaUpdates: false,
|
||||
// commonTrackFields: {
|
||||
// packaging: "loc",
|
||||
// renderGroup: 1,
|
||||
// },
|
||||
// tracks: [{
|
||||
// name: "tester",
|
||||
// namespace: "tester",
|
||||
// selectionParams: {}
|
||||
// }],
|
||||
// }
|
||||
// this.stream = null
|
||||
// this.subscriber = null
|
||||
|
||||
// this.#running = this.#run()
|
||||
// }
|
||||
|
||||
// static async init(config: BroadcasterConfig): Promise<Broadcast> {
|
||||
// const client = new Client({ url: config.url, fingerprint: config.fingerprint, role: "publisher" })
|
||||
// const connection = await client.connect();
|
||||
|
||||
// return new Broadcast({ connection, namespace: config.namespace })
|
||||
// }
|
||||
|
||||
// async #run() {
|
||||
// try {
|
||||
// await this.connection.announce(this.namespace)
|
||||
// this.subscribed = true
|
||||
// } catch (error) {
|
||||
|
||||
// this.subscribed = false
|
||||
// }
|
||||
|
||||
// for (; ;) {
|
||||
// const subscriber = await this.connection.subscribed()
|
||||
|
||||
// if (!subscriber) {
|
||||
// this.subscribed = false
|
||||
|
||||
// break
|
||||
// }
|
||||
|
||||
// await subscriber.ack()
|
||||
|
||||
// this.subscriber = subscriber
|
||||
|
||||
// this.subscribed = true
|
||||
|
||||
// const bytes = Catalog.encode(this.catalog);
|
||||
|
||||
// const stream = await subscriber.group({ group: 0 });
|
||||
|
||||
// await stream.write({ object: 0, payload: bytes })
|
||||
|
||||
// this.stream = stream
|
||||
// }
|
||||
// }
|
||||
|
||||
// isSubscribed(): boolean {
|
||||
// return this.subscribed;
|
||||
// }
|
||||
|
||||
// // async #serveSubscribe(subscriber: SubscribeRecv) {
|
||||
// // try {
|
||||
|
||||
// // // Send a SUBSCRIBE_OK
|
||||
// // await subscriber.ack()
|
||||
|
||||
// // console.log("catalog track name:", subscriber.track)
|
||||
|
||||
// // const stream = await subscriber.group({ group: 0 });
|
||||
|
||||
// // // const bytes = this.catalog.encode("Hello World")
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes })
|
||||
|
||||
|
||||
|
||||
// // } catch (e) {
|
||||
// // const err = asError(e)
|
||||
// // await subscriber.close(1n, `failed to process publish: ${err.message}`)
|
||||
// // } finally {
|
||||
// // // TODO we can't close subscribers because there's no support for clean termination
|
||||
// // // await subscriber.close()
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// // async mouseUpdatePosition({ x, y }: { x: number, y: number }, stream: GroupWriter) {
|
||||
|
||||
// // const mouse_move = {
|
||||
// // input_type: "mouse_move",
|
||||
// // delta_y: y,
|
||||
// // delta_x: x,
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async mouseUpdateButtons(e: MouseEvent, stream: GroupWriter) {
|
||||
// // const data: { input_type?: "mouse_key_down" | "mouse_key_up"; button: number; } = { button: e.button };
|
||||
|
||||
// // if (e.type === "mousedown") {
|
||||
// // data["input_type"] = "mouse_key_down"
|
||||
// // } else if (e.type === "mouseup") {
|
||||
// // data["input_type"] = "mouse_key_up"
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async mouseUpdateWheel(e: WheelEvent, stream: GroupWriter) {
|
||||
// // const data: { input_type?: "mouse_wheel_up" | "mouse_wheel_down" } = {}
|
||||
|
||||
// // if (e.deltaY < 0.0) {
|
||||
// // data["input_type"] = "mouse_wheel_up"
|
||||
// // } else {
|
||||
// // data["input_type"] = "mouse_wheel_down"
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async updateKeyUp(e: KeyboardEvent, stream: GroupWriter) {
|
||||
// // const data = {
|
||||
// // input_type: "key_up",
|
||||
// // key_code: e.keyCode
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// // async updateKeyDown(e: KeyboardEvent, stream: GroupWriter) {
|
||||
// // const data = {
|
||||
// // input_type: "key_down",
|
||||
// // key_code: e.keyCode
|
||||
// // }
|
||||
|
||||
// // const bytes = Catalog.encode(this.catalog)
|
||||
|
||||
// // await stream.write({ object: 0, payload: bytes });
|
||||
// // }
|
||||
|
||||
// close() {
|
||||
// // TODO implement publish close
|
||||
// }
|
||||
|
||||
// // Returns the error message when the connection is closed
|
||||
// async closed(): Promise<Error> {
|
||||
// try {
|
||||
// await this.#running
|
||||
// return new Error("closed") // clean termination
|
||||
// } catch (e) {
|
||||
// return asError(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,12 +1,5 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { HomeNavBar, Card } from "@nestri/ui";
|
||||
|
||||
function getGreeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 12) return "Good Morning";
|
||||
if (hour >= 12 && hour < 18) return "Good Afternoon";
|
||||
return "Good Evening";
|
||||
}
|
||||
import { HomeNavBar } from "@nestri/ui";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {useLocation} from "@builder.io/qwik-city";
|
||||
import {Keyboard, Mouse, WebRTCStream} from "@nestri/input"
|
||||
import {component$, useSignal, useVisibleTask$} from "@builder.io/qwik";
|
||||
import { useLocation } from "@builder.io/qwik-city";
|
||||
import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"
|
||||
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
|
||||
export default component$(() => {
|
||||
const id = useLocation().params.id;
|
||||
const canvas = useSignal<HTMLCanvasElement>();
|
||||
|
||||
useVisibleTask$(({track}) => {
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(({ track }) => {
|
||||
track(() => canvas.value);
|
||||
|
||||
if (!canvas.value) return; // Ensure canvas is available
|
||||
@@ -66,9 +67,9 @@ export default component$(() => {
|
||||
// @ts-ignore
|
||||
if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
window.nestrimouse = new Mouse({canvas: canvas.value, webrtc});
|
||||
window.nestrimouse = new Mouse({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
window.nestrikeyboard = new Keyboard({canvas: canvas.value, webrtc});
|
||||
window.nestrikeyboard = new Keyboard({ canvas: canvas.value, webrtc });
|
||||
// @ts-ignore
|
||||
} else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) {
|
||||
// @ts-ignore
|
||||
@@ -180,7 +181,7 @@ export default component$(() => {
|
||||
}
|
||||
}}
|
||||
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
|
||||
class="aspect-video h-full w-full object-contain max-h-screen"/>
|
||||
class="aspect-video h-full w-full object-contain max-h-screen" />
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { $, component$, noSerialize, type NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { TitleSection, MotionComponent, transition } from "@nestri/ui/react";
|
||||
import { NavBar, Footer, Book } from "@nestri/ui"
|
||||
import { cn } from "@nestri/ui/design";
|
||||
import { Howl, Howler } from 'howler';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
//FIXME: Add a FAQ section
|
||||
// FIXME: Takes too long for the price input radio input to become responsive
|
||||
@@ -68,6 +68,7 @@ export default component$(() => {
|
||||
const bookRef = useSignal<HTMLButtonElement | undefined>()
|
||||
const audio = useSignal<NoSerialize<Howl> | undefined>()
|
||||
|
||||
// eslint-disable-next-line qwik/no-use-visible-task
|
||||
useVisibleTask$(() => {
|
||||
audio.value = noSerialize(new Howl({ src: ["/audio/cash.mp3"], volume: 0.5 }))
|
||||
|
||||
|
||||
Vendored
+39
-1
@@ -1,4 +1,42 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
/* deno-fmt-ignore-file */
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-25
@@ -1,22 +1,15 @@
|
||||
{
|
||||
"extends": "@nestri/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"target": "ES2022",
|
||||
"target": "ES2017",
|
||||
"module": "ES2022",
|
||||
"lib": [
|
||||
"es2022",
|
||||
"DOM",
|
||||
"WebWorker",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@builder.io/qwik",
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
@@ -24,22 +17,9 @@
|
||||
"outDir": "tmp",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"content-collections": [
|
||||
"./.content-collections/generated"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.cjs"
|
||||
],
|
||||
"include": [
|
||||
"src",
|
||||
"./*.d.ts",
|
||||
"./*.config.ts",
|
||||
"./*.config.js",
|
||||
"content-collections.ts"
|
||||
, "../../packages/input/src/webrtc-stream.ts" ]
|
||||
"files": ["./.eslintrc.cjs"],
|
||||
"include": ["src", "./*.d.ts", "./*.config.ts","./*.config.cjs"]
|
||||
}
|
||||
+49
-19
@@ -1,22 +1,52 @@
|
||||
import { isPermanentStage } from "./stage";
|
||||
import { domain } from "./dns";
|
||||
import { secret } from "./secrets"
|
||||
|
||||
//TODO: Use this instead of wrangler
|
||||
// export const api = new sst.cloudflare.Worker("apiApi", {
|
||||
// url: true,
|
||||
// handler: "packages/api/src/index.ts",
|
||||
// // live: true,
|
||||
// });
|
||||
sst.Linkable.wrap(random.RandomString, (resource) => ({
|
||||
properties: {
|
||||
value: resource.result,
|
||||
},
|
||||
}));
|
||||
|
||||
if (!isPermanentStage) {
|
||||
new sst.x.DevCommand("apiDev", {
|
||||
dev: {
|
||||
command: "bun run dev",
|
||||
directory: "packages/api",
|
||||
autostart: true,
|
||||
},
|
||||
})
|
||||
export const authFingerprintKey = new random.RandomString(
|
||||
"AuthFingerprintKey",
|
||||
{
|
||||
length: 32,
|
||||
},
|
||||
);
|
||||
|
||||
export const urls = new sst.Linkable("Urls", {
|
||||
properties: {
|
||||
api: "https://api." + domain,
|
||||
auth: "https://auth." + domain,
|
||||
},
|
||||
});
|
||||
|
||||
export const kv = new sst.cloudflare.Kv("CloudflareAuthKV")
|
||||
|
||||
export const auth = new sst.cloudflare.Worker("Auth", {
|
||||
link: [
|
||||
kv,
|
||||
urls,
|
||||
authFingerprintKey,
|
||||
secret.InstantAdminToken,
|
||||
secret.InstantAppId,
|
||||
secret.LoopsApiKey
|
||||
],
|
||||
handler: "./packages/functions/src/auth.ts",
|
||||
url: true,
|
||||
domain: "auth." + domain
|
||||
});
|
||||
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
link: [
|
||||
urls,
|
||||
],
|
||||
url: true,
|
||||
handler: "./packages/functions/src/api/index.ts",
|
||||
domain: "api." + domain
|
||||
})
|
||||
|
||||
export const outputs = {
|
||||
auth: auth.url,
|
||||
api: api.url
|
||||
}
|
||||
|
||||
// export const outputs = {
|
||||
// api: api.url
|
||||
// }
|
||||
@@ -0,0 +1,10 @@
|
||||
import { auth, urls } from "./api"
|
||||
|
||||
|
||||
// export const cmd = new sst.x.DevCommand("Cmd", {
|
||||
// link: [urls, auth],
|
||||
// dev: {
|
||||
// autostart: true,
|
||||
// command: "cd packages/cmd && go run main.go"
|
||||
// }
|
||||
// })
|
||||
@@ -0,0 +1,9 @@
|
||||
export const domain =
|
||||
{
|
||||
production: "prod.nestri.io", //temporary use until we go into the real production
|
||||
dev: "dev.nestri.io",
|
||||
}[$app.stage] || $app.stage + ".dev.nestri.io";
|
||||
|
||||
export const zone = cloudflare.getZoneOutput({
|
||||
name: "nestri.io",
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
// export const domain =
|
||||
// {
|
||||
// production: "fst.so",
|
||||
// dev: "dev.fst.so",
|
||||
// }[$app.stage] || $app.stage + ".dev.fst.so";
|
||||
|
||||
// export const zone = cloudflare.getZoneOutput({
|
||||
// name: "fst.so",
|
||||
// });
|
||||
@@ -1,22 +0,0 @@
|
||||
import { resolve as pathResolve } from "node:path";
|
||||
import { readFileSync as readFile } from "node:fs";
|
||||
//Copy your (known) ssh public key to the remote machine
|
||||
//ssh-copy-id "-p $port" user@host
|
||||
|
||||
const domain = "fst.so"
|
||||
const ips = ["95.216.29.238"]
|
||||
|
||||
// Get the hosted zone
|
||||
const zone = aws.route53.getZone({ name: domain });
|
||||
|
||||
// Create an A record
|
||||
const record = new aws.route53.Record("Relay DNS Records", {
|
||||
zoneId: zone.then(zone => zone.zoneId),
|
||||
type: "A",
|
||||
name: `relay.${domain}`,
|
||||
ttl: 300,
|
||||
records: ips,
|
||||
});
|
||||
|
||||
// Export the URL
|
||||
export const url = $interpolate`https://${record.name}`;
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
export const secret = {
|
||||
CloudflareAccountIdSecret: new sst.Secret("CloudflareAccountId"),
|
||||
};
|
||||
InstantAdminToken: new sst.Secret("InstantAdminToken"),
|
||||
InstantAppId: new sst.Secret("InstantAppId"),
|
||||
LoopsApiKey: new sst.Secret("LoopsApiKey")
|
||||
};
|
||||
|
||||
export const allSecrets = Object.values(secret);
|
||||
export const allSecrets = Object.values(secret);
|
||||
@@ -1,79 +0,0 @@
|
||||
//Deploys the website to cloudflare pages under the domain nestri.io (redirects all requests to www.nestri.io to avoid duplicate content)
|
||||
|
||||
import { isPermanentStage } from "./stage";
|
||||
|
||||
export const www = new cloudflare.PagesProject("www", {
|
||||
name: "nestri",
|
||||
accountId: "8405b2acb6746935b975bc2cfcb5c288",
|
||||
productionBranch: "main",
|
||||
buildConfig: {
|
||||
rootDir: "apps/www",
|
||||
buildCommand: "bun run build",
|
||||
destinationDir: "dist"
|
||||
},
|
||||
deploymentConfigs: {
|
||||
production: {
|
||||
compatibilityFlags: ["nodejs_compat"]
|
||||
},
|
||||
preview: {
|
||||
compatibilityFlags: ["nodejs_compat"]
|
||||
}
|
||||
},
|
||||
source: {
|
||||
type: "github",
|
||||
config: {
|
||||
owner: "nestriness",
|
||||
deploymentsEnabled: true,
|
||||
productionBranch: "main",
|
||||
repoName: "nestri",
|
||||
productionDeploymentEnabled: true,
|
||||
prCommentsEnabled: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//TODO: Maybe handle building Qwik ourselves? This prevents us from relying on CF too much, we are open-source anyway 🤷🏾♂️
|
||||
//TODO: Add a local dev server for Qwik that can be linked with whatever we want
|
||||
//TODO: Link the www PageRule with whatever we give to the local dev server
|
||||
if (!isPermanentStage) {
|
||||
new sst.x.DevCommand("www", {
|
||||
dev: {
|
||||
command: "bun run dev",
|
||||
directory: "apps/www",
|
||||
autostart: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// //This creates a resource that can be accessed by itself
|
||||
// new sst.Linkable.wrap(cloudflare.PageRule, (resource) => ({
|
||||
// // these properties will be available when linked
|
||||
// properties: {
|
||||
// arn: resource.urn
|
||||
// }
|
||||
// }))
|
||||
// //And then you call your linkable resource like this:
|
||||
// // const www = cloudflare.PageRule("www", {})
|
||||
|
||||
// //this creates a linkable resource that can be linked to other resources
|
||||
// export const linkable2 = new sst.Linkable("ExistingResource", {
|
||||
// properties: {
|
||||
// arn: "arn:aws:s3:::nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z"
|
||||
// },
|
||||
// include: [
|
||||
// sst.aws.permission({
|
||||
// actions: ["s3:*"],
|
||||
// resources: ["arn:aws:s3:::nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z"]
|
||||
// }),
|
||||
// sst.cloudflare.binding({
|
||||
// type: "r2BucketBindings",
|
||||
// properties: {
|
||||
// bucketName: "nestri-website-artifacts-prod-nestri-io-01h70zg50qz5z",
|
||||
// }
|
||||
// })
|
||||
// ]
|
||||
// })
|
||||
|
||||
export const outputs = {
|
||||
www: www.subdomain,
|
||||
};
|
||||
+2
-1
@@ -4,6 +4,7 @@
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
"sst": "sst dev",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"lint": "turbo lint"
|
||||
},
|
||||
@@ -29,6 +30,6 @@
|
||||
"workerd"
|
||||
],
|
||||
"dependencies": {
|
||||
"sst": "3.0.94"
|
||||
"sst": "^3.4.32"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# prod
|
||||
dist/
|
||||
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
.wrangler
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
.dev.vars
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -1,12 +0,0 @@
|
||||
# Nexus
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
npm run deploy
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="48.672001"
|
||||
height="36.804001"
|
||||
viewBox="0 0 12.8778 9.7377253"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="layer1">
|
||||
<path
|
||||
d="m 2.093439,1.7855532 h 8.690922 V 2.2639978 H 2.093439 Z m 0,2.8440874 h 8.690922 V 5.1080848 H 2.093439 Z m 0,2.8440866 h 8.690922 V 7.952172 H 2.093439 Z"
|
||||
style="font-size:12px;fill:#ff4f01;fill-opacity:1;fill-rule:evenodd;stroke:#ff4f01;stroke-width:1.66201;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 590 B |
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "@nestri/nexus",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Nestri's core",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev src/index.ts",
|
||||
"deploy": "wrangler deploy --minify src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cf-wasm/resvg": "^0.1.21",
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"@jsquash/avif": "^1.3.0",
|
||||
"@jsquash/jpeg": "^1.4.0",
|
||||
"@jsquash/resize": "^2.0.0",
|
||||
"@nestri/cache": "*",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"wrangler": "^3.72.2"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Resvg } from "@cf-wasm/resvg";
|
||||
import { generateGradient, createAvatarSvg } from '../utils';
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "avatar-cache"
|
||||
};
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const [name, fileType] = id.split('.');
|
||||
const size = parseInt(c.req.query("size") || "200");
|
||||
|
||||
const validImageTypes = ["png"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(fileType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const gradient = generateGradient(name || Math.random() + "");
|
||||
// console.log(`gradient: ${JSON.stringify(gradient)}`)
|
||||
const svg = createAvatarSvg(size, gradient, fileType);
|
||||
const resvg = await Resvg.create(svg.toString());
|
||||
const pngData = resvg.render()
|
||||
const pngBuffer = pngData.asPng()
|
||||
|
||||
return c.newResponse(pngBuffer, 200, {
|
||||
"Content-Type": `image/${fileType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
import resize, { initResize } from '@jsquash/resize';
|
||||
import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode';
|
||||
import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js';
|
||||
import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm";
|
||||
import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm";
|
||||
import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm";
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "cover-cache"
|
||||
};
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
const decodeImage = async (buffer: ArrayBuffer) => {
|
||||
await initDecodeJpeg(JPEG_DEC_WASM);
|
||||
return decodeJpeg(buffer);
|
||||
}
|
||||
|
||||
const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => {
|
||||
await initResize(RESIZE_WASM);
|
||||
// Resize image with respect to aspect ratio
|
||||
const aspectRatio = image.width / image.height;
|
||||
const newWidth = width;
|
||||
const newHeight = width / aspectRatio;
|
||||
return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" });
|
||||
}
|
||||
|
||||
const encodeImage = async (image: { width: number; height: number }, format: string) => {
|
||||
if (format === 'avif') {
|
||||
await initEncodeAvif(AVIF_ENC_WASM);
|
||||
return encodeAvif(image);
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`);
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const [gameId, imageType] = c.req.param("id").split('.');
|
||||
const width = parseInt(c.req.query("width") || "460");
|
||||
//We don't even use this, but let us keep it for future use
|
||||
const height = parseInt(c.req.query("height") || "215");
|
||||
if (!gameId || !imageType) {
|
||||
return c.text("Invalid image parameters", 400)
|
||||
}
|
||||
//Support Avif only because of it's small size
|
||||
const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(imageType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/header.jpg`;
|
||||
const image = await fetch(imageUrl);
|
||||
if (!image.ok) {
|
||||
return c.text('Image not found', 404);
|
||||
}
|
||||
const imageBuffer = await image.arrayBuffer();
|
||||
const imageData = await decodeImage(imageBuffer);
|
||||
const resizedImage = await resizeImage(imageData, width, height);
|
||||
const resizedImageBuffer = await encodeImage(resizedImage, imageType);
|
||||
return c.newResponse(resizedImageBuffer, 200, {
|
||||
"Content-Type": `image/${imageType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
import { kvCaches } from "@nestri/cache"
|
||||
import resize, { initResize } from '@jsquash/resize';
|
||||
import decodeJpeg, { init as initDecodeJpeg } from '@jsquash/jpeg/decode';
|
||||
import encodeAvif, { init as initEncodeAvif } from '@jsquash/avif/encode.js';
|
||||
import JPEG_DEC_WASM from "@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm";
|
||||
import RESIZE_WASM from "@jsquash/resize/lib/resize/pkg/squoosh_resize_bg.wasm";
|
||||
import AVIF_ENC_WASM from "@jsquash/avif/codec/enc/avif_enc.wasm";
|
||||
|
||||
const cacheOptions = {
|
||||
key: "nexus",
|
||||
namespace: "cover-cache"
|
||||
};
|
||||
|
||||
const middleware = kvCaches(cacheOptions);
|
||||
|
||||
const decodeImage = async (buffer: ArrayBuffer) => {
|
||||
await initDecodeJpeg(JPEG_DEC_WASM);
|
||||
return decodeJpeg(buffer);
|
||||
}
|
||||
|
||||
const resizeImage = async (image: { width: number; height: number }, width: number, height: number) => {
|
||||
await initResize(RESIZE_WASM);
|
||||
// Resize image with respect to aspect ratio
|
||||
const aspectRatio = image.width / image.height;
|
||||
const newWidth = width;
|
||||
const newHeight = width / aspectRatio;
|
||||
return resize(image, { width: newWidth, height: newHeight, fitMethod: "stretch" });
|
||||
}
|
||||
|
||||
const encodeImage = async (image: { width: number; height: number }, format: string) => {
|
||||
if (format === 'avif') {
|
||||
await initEncodeAvif(AVIF_ENC_WASM);
|
||||
return encodeAvif(image);
|
||||
}
|
||||
throw new Error(`Unsupported image format: ${format}`);
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const [gameId, imageType] = c.req.param("id").split('.');
|
||||
const width = parseInt(c.req.query("width") || "600");
|
||||
//We don't even use this, but let us keep it for future use
|
||||
const height = parseInt(c.req.query("height") || "900");
|
||||
if (!gameId || !imageType) {
|
||||
return c.text("Invalid image parameters", 400)
|
||||
}
|
||||
//Support Avif only because of it's small size
|
||||
const validImageTypes = ["avif"] //['jpg', 'png', 'webp', 'avif'];
|
||||
if (!validImageTypes.includes(imageType)) {
|
||||
return c.text('Invalid image type', 400);
|
||||
}
|
||||
|
||||
const imageUrl = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${gameId}/library_600x900_2x.jpg`;
|
||||
const image = await fetch(imageUrl);
|
||||
if (!image.ok) {
|
||||
return c.text('Image not found', 404);
|
||||
}
|
||||
const imageBuffer = await image.arrayBuffer();
|
||||
const imageData = await decodeImage(imageBuffer);
|
||||
const resizedImage = await resizeImage(imageData, width, height);
|
||||
const resizedImageBuffer = await encodeImage(resizedImage, imageType);
|
||||
return c.newResponse(resizedImageBuffer, 200, {
|
||||
"Content-Type": `image/${imageType}`,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable'
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import Cover from './cover'
|
||||
import Avatar from './avatar'
|
||||
import Banner from './banner'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.text('Hello There! 👋🏾')
|
||||
})
|
||||
|
||||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.use(
|
||||
'/*',
|
||||
cors({
|
||||
origin: (origin, c) => {
|
||||
const allowedOriginPatterns = [
|
||||
/^https:\/\/.*\.nestri\.pages\.dev$/,
|
||||
/^https:\/\/.*\.nestri\.io$/,
|
||||
/^http:\/\/localhost:\d+$/ // For local development
|
||||
];
|
||||
|
||||
return allowedOriginPatterns.some(pattern => pattern.test(origin))
|
||||
? origin
|
||||
: 'https://nexus.nestri.io'
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
app.route('/cover', Cover)
|
||||
|
||||
app.route('/avatar', Avatar)
|
||||
|
||||
app.route("/banner", Banner)
|
||||
|
||||
export default app
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
import Image from "./image"
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.text('Hello There! 👋🏾')
|
||||
})
|
||||
|
||||
app.get('/favicon.ico', (c) => {
|
||||
return c.newResponse(null, 302, {
|
||||
Location: 'https://nestri.pages.dev/favicon.svg'
|
||||
})
|
||||
})
|
||||
|
||||
app.route("/image", Image)
|
||||
|
||||
export default app
|
||||
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
declare module '*wasm' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export const createAvatarSvg = (size: number, gradient: { fromColor: string, toColor: string }, fileType?: string, text?: string) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor={gradient.fromColor} />
|
||||
<stop offset="100%" stopColor={gradient.toColor} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill="url(#gradient)" x="0" y="0" width={size} height={size} />
|
||||
{fileType === "svg" && text && (
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
alignmentBaseline="central"
|
||||
dominantBaseline="central"
|
||||
textAnchor="middle"
|
||||
fill="#fff"
|
||||
fontFamily="sans-serif"
|
||||
fontSize={(size * 0.9) / text.length}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,24 +0,0 @@
|
||||
import color from "tinycolor2";
|
||||
|
||||
function fnv1a(str: string) {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
export function generateGradient(username: string) {
|
||||
const hash = fnv1a(username);
|
||||
const hue1 = hash % 360;
|
||||
const hue2 = (hash >> 16) % 360;
|
||||
|
||||
const c1 = color({ h: hue1, s: 0.8, l: 0.6 });
|
||||
const c2 = color({ h: hue2, s: 0.8, l: 0.5 });
|
||||
|
||||
return {
|
||||
fromColor: c1.toHexString(),
|
||||
toColor: c2.toHexString(),
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './gradient'
|
||||
export * from './create-avatar'
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
name = "nexus"
|
||||
compatibility_date = "2024-08-14"
|
||||
send_metrics = false
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "nexus"
|
||||
id = "e21527f9f1ed4adbaca1fa23d7f147c9"
|
||||
|
||||
# [vars]
|
||||
# MY_VAR = "my-variable"
|
||||
|
||||
# [[kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# [[r2_buckets]]
|
||||
# binding = "MY_BUCKET"
|
||||
# bucket_name = "my-bucket"
|
||||
|
||||
# [[d1_databases]]
|
||||
# binding = "DB"
|
||||
# database_name = "my-database"
|
||||
# database_id = ""
|
||||
|
||||
# [ai]
|
||||
# binding = "AI"
|
||||
Vendored
-47
@@ -1,47 +0,0 @@
|
||||
const buildCacheKey = (namespace: string) => (key: string) => {
|
||||
return `${namespace}:${key}`;
|
||||
};
|
||||
|
||||
export interface KVResponseCache {
|
||||
match(key: string): Promise<Response | null>;
|
||||
put(key: string, res: Response, options?: KVNamespacePutOptions): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const kvResponseCache =
|
||||
(kv: KVNamespace) =>
|
||||
(cacheName: string): KVResponseCache => {
|
||||
const cacheKey = buildCacheKey(cacheName);
|
||||
|
||||
return {
|
||||
async match(key: string) {
|
||||
const [headers, status, body] = await Promise.all([
|
||||
kv.get(cacheKey(`${key}:headers`)),
|
||||
kv.get(cacheKey(`${key}:status`)),
|
||||
kv.get(cacheKey(`${key}:body`), "stream"),
|
||||
]);
|
||||
|
||||
if (headers === null || body === null || status === null) return null;
|
||||
|
||||
return new Response(body, { headers: JSON.parse(headers), status: parseInt(status, 10) });
|
||||
},
|
||||
async put(key: string, res: Response, options?: KVNamespacePutOptions) {
|
||||
const headers = Array.from(res.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||
const body = res.body;
|
||||
if (body === null) return;
|
||||
|
||||
await Promise.all([
|
||||
kv.put(cacheKey(`${key}:headers`), JSON.stringify(headers), options),
|
||||
kv.put(cacheKey(`${key}:status`), `${res.status}`, options),
|
||||
kv.put(cacheKey(`${key}:body`), body, options),
|
||||
]);
|
||||
},
|
||||
async delete(key: string) {
|
||||
await Promise.all([
|
||||
kv.delete(cacheKey(`${key}:headers`)),
|
||||
kv.delete(cacheKey(`${key}:status`)),
|
||||
kv.delete(cacheKey(`${key}:body`)),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
//copied from https://github.com/napolab/kv-response-cache with some minor changes
|
||||
import { kvResponseCache } from "./caches";
|
||||
import { kvCaches, defaultGetCacheKey } from "./middleware";
|
||||
|
||||
import type { KVResponseCache } from "./caches";
|
||||
|
||||
export type { KVResponseCache };
|
||||
export { kvResponseCache, kvCaches, defaultGetCacheKey as getCacheKey };
|
||||
Vendored
-47
@@ -1,47 +0,0 @@
|
||||
import { kvResponseCache } from "./caches";
|
||||
|
||||
import type { Filter } from "./types";
|
||||
import type { Context, Env, MiddlewareHandler } from "hono";
|
||||
|
||||
type Namespace<E extends Env> = string | ((c: Context<E>) => string);
|
||||
interface GetCacheKey<E extends Env> {
|
||||
(c: Context<E>): string;
|
||||
}
|
||||
|
||||
type KVCacheOption<E extends Env & { Bindings: Record<string, unknown> }> = {
|
||||
key: keyof E["Bindings"];
|
||||
namespace: Namespace<E>;
|
||||
getCacheKey?: GetCacheKey<E>;
|
||||
options?: KVNamespacePutOptions;
|
||||
};
|
||||
|
||||
export const defaultGetCacheKey = <E extends Env>(c: Context<E>) => c.req.url;
|
||||
|
||||
export const kvCaches =
|
||||
<E extends Env & { Bindings: Record<string, unknown> }>({
|
||||
key: bindingKey,
|
||||
namespace,
|
||||
options,
|
||||
getCacheKey = defaultGetCacheKey,
|
||||
}: KVCacheOption<E>): MiddlewareHandler<E> =>
|
||||
async (c, next) => {
|
||||
const kv: KVNamespace = c.env?.[bindingKey] as KVNamespace;
|
||||
const kvNamespace = typeof namespace === "function" ? namespace(c) : namespace;
|
||||
|
||||
const kvCaches = kvResponseCache(kv);
|
||||
const cache = kvCaches(kvNamespace);
|
||||
|
||||
const key = getCacheKey(c);
|
||||
const response = await cache.match(key);
|
||||
if (response) {
|
||||
response.headers.set("X-KV-CACHE", "hit");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
await next();
|
||||
|
||||
if (c.res.status >= 200 && c.res.status < 300) {
|
||||
c.executionCtx.waitUntil(cache.put(key, c.res.clone(), options));
|
||||
}
|
||||
};
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@nestri/cache",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
}
|
||||
}
|
||||
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
]
|
||||
},
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
export type Filter<T extends Record<string, unknown>, V> = {
|
||||
[K in keyof T as T[K] extends V ? K : never]: T[K];
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
.terraform
|
||||
relay_*
|
||||
terraform.tfstate
|
||||
Generated
-61
@@ -1,61 +0,0 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/local" {
|
||||
version = "2.5.2"
|
||||
hashes = [
|
||||
"h1:JlMZD6nYqJ8sSrFfEAH0Vk/SL8WLZRmFaMUF9PJK5wM=",
|
||||
"zh:136299545178ce281c56f36965bf91c35407c11897f7082b3b983d86cb79b511",
|
||||
"zh:3b4486858aa9cb8163378722b642c57c529b6c64bfbfc9461d940a84cd66ebea",
|
||||
"zh:4855ee628ead847741aa4f4fc9bed50cfdbf197f2912775dd9fe7bc43fa077c0",
|
||||
"zh:4b8cd2583d1edcac4011caafe8afb7a95e8110a607a1d5fb87d921178074a69b",
|
||||
"zh:52084ddaff8c8cd3f9e7bcb7ce4dc1eab00602912c96da43c29b4762dc376038",
|
||||
"zh:71562d330d3f92d79b2952ffdda0dad167e952e46200c767dd30c6af8d7c0ed3",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:805f81ade06ff68fa8b908d31892eaed5c180ae031c77ad35f82cb7a74b97cf4",
|
||||
"zh:8b6b3ebeaaa8e38dd04e56996abe80db9be6f4c1df75ac3cccc77642899bd464",
|
||||
"zh:ad07750576b99248037b897de71113cc19b1a8d0bc235eb99173cc83d0de3b1b",
|
||||
"zh:b9f1c3bfadb74068f5c205292badb0661e17ac05eb23bfe8bd809691e4583d0e",
|
||||
"zh:cc4cbcd67414fefb111c1bf7ab0bc4beb8c0b553d01719ad17de9a047adff4d1",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/tls" {
|
||||
version = "4.0.6"
|
||||
hashes = [
|
||||
"h1:dYSb3V94K5dDMtrBRLPzBpkMTPn+3cXZ/kIJdtFL+2M=",
|
||||
"zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8",
|
||||
"zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297",
|
||||
"zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb",
|
||||
"zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1",
|
||||
"zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509",
|
||||
"zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8",
|
||||
"zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a",
|
||||
"zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18",
|
||||
"zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50",
|
||||
"zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27",
|
||||
"zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/vancluever/acme" {
|
||||
version = "2.26.0"
|
||||
constraints = "~> 2.0"
|
||||
hashes = [
|
||||
"h1:4Lk5cb2Fg1q1JEQf1jkrShjPC3ayukp4eFcdL4e+y0w=",
|
||||
"zh:11f554916ee99d8930de6d7bb5a014ec636b53ef9ba35eea84b0d2522c78230f",
|
||||
"zh:231c31271c25477c95e0a4972857b6d5e9d7c3a300cbc4b0948566d87bc46e04",
|
||||
"zh:2ae165ca7a994a4c77801a82ebd9f2f6de33a4c8882381bea575b3385cc251d8",
|
||||
"zh:2cf01e4694d81b24972f5dab8e5f374aa59100082ff6e2435615d9c0f24cc00e",
|
||||
"zh:3de6f6f9d052dfaa5d5f366d7ca26bdebb42fc74b6e19325e67420c37ff630d3",
|
||||
"zh:3fd2b4b680b970394e4d0d49c2a8e5365297e79cea418ce87197cc8bb456d8c7",
|
||||
"zh:46ea249cc01dce23ff6c8f02106e693be3b046059834b60b670c45a8f4093980",
|
||||
"zh:57cb181c73b6e7397744d885c788d8815ad6a43f07769e98c6327bbc37272896",
|
||||
"zh:761f2adf3e63559bd279763eb91247cdebf31401d79853755453274f143cbb36",
|
||||
"zh:c4a9905bf81d38201c080cb91ea85002194c47ca26619644628184a56c394b7d",
|
||||
"zh:d6e3a757c357239edefb640807778fb69805b9ae5df84a811a2d505c51089367",
|
||||
"zh:d713856e4a459e1091cbb19ffb830d25cd88953d3e54acd46db0729c77a531d8",
|
||||
"zh:f7cb8dec263d0ee223737dad3b6fa8071258f41cfa9e0b8cf7f337f9f501fc3b",
|
||||
]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
## Usage
|
||||
1. Update the terraform.tfvars file with your domain and email.
|
||||
|
||||
2. Run `terraform init` to initialize the Terraform working directory.
|
||||
|
||||
3. Run `terraform plan` to see the planned changes.
|
||||
|
||||
4. Run `terraform apply` to create the resources and obtain the certificate.
|
||||
Outputs
|
||||
|
||||
The configuration provides two sensitive outputs:
|
||||
```bash
|
||||
certificate_pem: The full certificate chain
|
||||
private_key_pem: The private key for the certificate
|
||||
```
|
||||
|
||||
These can be then be used in your `moq-relay` as it requires SSL/TLS certificates.
|
||||
|
||||
## Note
|
||||
The generated certificate and key files are saved locally and ignored by git:
|
||||
```git
|
||||
.terraform
|
||||
relay_*
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
variable "email" {
|
||||
description = "Your email address, used for LetsEncrypt"
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "domain name"
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
acme = {
|
||||
source = "vancluever/acme"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "acme" {
|
||||
server_url = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
}
|
||||
|
||||
resource "acme_registration" "reg" {
|
||||
email_address = "wanjohiryan33@gmail.com"
|
||||
}
|
||||
|
||||
resource "tls_private_key" "relay" {
|
||||
algorithm = "ECDSA"
|
||||
ecdsa_curve = "P256"
|
||||
}
|
||||
|
||||
resource "acme_registration" "relay" {
|
||||
account_key_pem = tls_private_key.relay.private_key_pem
|
||||
email_address = var.email
|
||||
}
|
||||
|
||||
resource "acme_certificate" "relay" {
|
||||
account_key_pem = acme_registration.relay.account_key_pem
|
||||
common_name = "relay.${var.domain}"
|
||||
subject_alternative_names = ["*.relay.${var.domain}"]
|
||||
key_type = tls_private_key.relay.ecdsa_curve
|
||||
|
||||
recursive_nameservers = ["8.8.8.8:53"]
|
||||
|
||||
dns_challenge {
|
||||
provider = "route53"
|
||||
}
|
||||
}
|
||||
|
||||
# New resources to save certificate and private key
|
||||
resource "local_file" "cert_file" {
|
||||
content = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}"
|
||||
filename = "${path.module}/relay_cert.crt"
|
||||
file_permission = "0644"
|
||||
directory_permission = "0755"
|
||||
}
|
||||
|
||||
resource "local_file" "key_file" {
|
||||
content = acme_certificate.relay.private_key_pem
|
||||
filename = "${path.module}/relay_key.key"
|
||||
file_permission = "0600"
|
||||
directory_permission = "0755"
|
||||
}
|
||||
|
||||
# Outputs for certificate and private key
|
||||
output "certificate_pem" {
|
||||
value = "${acme_certificate.relay.certificate_pem}${acme_certificate.relay.issuer_pem}"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "private_key_pem" {
|
||||
value = acme_certificate.relay.private_key_pem
|
||||
sensitive = true
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
domain = "fst.so"
|
||||
email = "wanjohiryan33@gmail.com"
|
||||
@@ -0,0 +1,50 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "nestri",
|
||||
Short: "A CLI tool to run and manage your self-hosted cloud gaming service",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() error {
|
||||
err := rootCmd.Execute()
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
// Version stores the build version of VHS at the time of package through
|
||||
// -ldflags.
|
||||
//
|
||||
// go build -ldflags "-s -w -X=main.Version=$(VERSION)"
|
||||
Version string
|
||||
|
||||
// CommitSHA stores the git commit SHA at the time of package through -ldflags.
|
||||
CommitSHA string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runCmd)
|
||||
if len(CommitSHA) >= 7 { //nolint:gomnd
|
||||
vt := rootCmd.VersionTemplate()
|
||||
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
|
||||
}
|
||||
if Version == "" {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" {
|
||||
Version = info.Main.Version
|
||||
} else {
|
||||
Version = "unknown (built from source)"
|
||||
}
|
||||
}
|
||||
rootCmd.Version = Version
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"nestrilabs/cli/internal/auth"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run a new Nestri node",
|
||||
Long: "Create and run a new Nestri node from this machine",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
credentials, err := auth.FetchUserCredentials()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Credentials", "access_token", credentials.AccessToken)
|
||||
log.Info("Credentials", "refresh_token", credentials.RefreshToken)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
module nestrilabs/cli
|
||||
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/docker/docker v27.4.1+incompatible
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.13.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
@@ -2,22 +2,33 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
|
||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
|
||||
github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -29,17 +40,29 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3 h1:IqtLHbOF3y/SD3riYYKauQKj9dpqU7uuEExqL5zQ390=
|
||||
github.com/nestrilabs/nestri-go-sdk v0.1.0-alpha.3/go.mod h1:b4AuAQSxfqtAzu4ie0Q+NOVNF9YUZTyP4XnxK0ZN05U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
@@ -49,42 +72,63 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -93,12 +137,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -109,14 +154,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nestrilabs/cli/internal/resource"
|
||||
|
||||
"github.com/nestrilabs/nestri-go-sdk"
|
||||
"github.com/nestrilabs/nestri-go-sdk/option"
|
||||
)
|
||||
|
||||
func RegisterMachine(token string) {
|
||||
client := nestri.NewClient(
|
||||
option.WithBearerToken(token),
|
||||
option.WithBaseURL(resource.Resource.Api.Url),
|
||||
)
|
||||
|
||||
machine, err := client.Machines.New(
|
||||
context.TODO(),
|
||||
nestri.MachineNewParams{})
|
||||
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
fmt.Printf("%+v\n", machine.Data)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"nestrilabs/cli/internal/machine"
|
||||
"nestrilabs/cli/internal/resource"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type UserCredentials struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func FetchUserCredentials() (*UserCredentials, error) {
|
||||
m := machine.NewMachine()
|
||||
fingerprint := m.GetMachineID()
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "client_credentials")
|
||||
data.Set("client_id", "device")
|
||||
data.Set("client_secret", resource.Resource.AuthFingerprintKey.Value)
|
||||
data.Set("hostname", m.Hostname)
|
||||
data.Set("fingerprint", fingerprint)
|
||||
data.Set("provider", "device")
|
||||
resp, err := http.PostForm(resource.Resource.Auth.Url+"/token", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println(string(body))
|
||||
return nil, fmt.Errorf("failed to auth: " + string(body))
|
||||
}
|
||||
credentials := UserCredentials{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &credentials, nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
type Machine struct {
|
||||
OperatingSystem string
|
||||
Arch string
|
||||
Kernel string
|
||||
Virtualization string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func NewMachine() *Machine {
|
||||
var OS string
|
||||
var architecture string
|
||||
var kernel string
|
||||
var virtualisation string
|
||||
var hostname string
|
||||
|
||||
output, _ := exec.Command("hostnamectl", "status").Output()
|
||||
os := regexp.MustCompile(`Operating System:\s+(.*)`)
|
||||
matchingOS := os.FindStringSubmatch(string(output))
|
||||
if len(matchingOS) > 1 {
|
||||
OS = matchingOS[1]
|
||||
}
|
||||
|
||||
arch := regexp.MustCompile(`Architecture:\s+(\w+)`)
|
||||
matchingArch := arch.FindStringSubmatch(string(output))
|
||||
if len(matchingArch) > 1 {
|
||||
architecture = matchingArch[1]
|
||||
}
|
||||
|
||||
kern := regexp.MustCompile(`Kernel:\s+(.*)`)
|
||||
matchingKernel := kern.FindStringSubmatch(string(output))
|
||||
if len(matchingKernel) > 1 {
|
||||
kernel = matchingKernel[1]
|
||||
}
|
||||
|
||||
virt := regexp.MustCompile(`Virtualization:\s+(\w+)`)
|
||||
matchingVirt := virt.FindStringSubmatch(string(output))
|
||||
if len(matchingVirt) > 1 {
|
||||
virtualisation = matchingVirt[1]
|
||||
}
|
||||
|
||||
host := regexp.MustCompile(`Static hostname:\s+(.*)`)
|
||||
matchingHost := host.FindStringSubmatch(string(output))
|
||||
if len(matchingHost) > 1 {
|
||||
hostname = matchingHost[1]
|
||||
}
|
||||
|
||||
return &Machine{
|
||||
OperatingSystem: OS,
|
||||
Arch: architecture,
|
||||
Kernel: kernel,
|
||||
Virtualization: virtualisation,
|
||||
Hostname: hostname,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Machine) GetOS() string {
|
||||
if m.OperatingSystem != "" {
|
||||
return m.OperatingSystem
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (m *Machine) GetArchitecture() string {
|
||||
|
||||
if m.Arch != "" {
|
||||
return m.Arch
|
||||
}
|
||||
return "unknown"
|
||||
|
||||
}
|
||||
|
||||
func (m *Machine) GetKernel() string {
|
||||
if m.Kernel != "" {
|
||||
return m.Kernel
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (m *Machine) GetVirtualization() string {
|
||||
if m.Virtualization != "" {
|
||||
return m.Virtualization
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (m *Machine) GetHostname() string {
|
||||
if m.Hostname != "" {
|
||||
return m.Hostname
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (m *Machine) GetMachineID() string {
|
||||
id, err := os.ReadFile("/etc/machine-id")
|
||||
if err != nil {
|
||||
log.Error("Error getting your machine's ID", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return strings.TrimSpace(string(id))
|
||||
}
|
||||
|
||||
func (m *Machine) GPUInfo() (string, string, error) {
|
||||
// The command for GPU information varies depending on the system and drivers.
|
||||
// lshw is a good general-purpose tool, but might need adjustments for specific hardware.
|
||||
output, err := exec.Command("lshw", "-C", "display").Output()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get GPU information: %w", err)
|
||||
}
|
||||
|
||||
gpuType := ""
|
||||
gpuSize := ""
|
||||
|
||||
// Regular expressions for extracting product and size information. These might need to be
|
||||
// adapted based on the output of lshw on your specific system.
|
||||
typeRegex := regexp.MustCompile(`product:\s+(.*)`)
|
||||
sizeRegex := regexp.MustCompile(`size:\s+(\d+MiB)`) // Example: extracts size in MiB
|
||||
|
||||
typeMatch := typeRegex.FindStringSubmatch(string(output))
|
||||
if len(typeMatch) > 1 {
|
||||
gpuType = typeMatch[1]
|
||||
}
|
||||
|
||||
sizeMatch := sizeRegex.FindStringSubmatch(string(output))
|
||||
if len(sizeMatch) > 1 {
|
||||
gpuSize = sizeMatch[1]
|
||||
}
|
||||
|
||||
if gpuType == "" && gpuSize == "" {
|
||||
return "", "", fmt.Errorf("could not parse GPU information using lshw")
|
||||
}
|
||||
|
||||
return gpuType, gpuSize, nil
|
||||
}
|
||||
|
||||
func (m *Machine) GetCPUInfo() (string, string, error) {
|
||||
output, err := exec.Command("lscpu").Output()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get CPU information: %w", err)
|
||||
}
|
||||
|
||||
cpuType := ""
|
||||
cpuSize := "" // This will store the number of cores
|
||||
|
||||
typeRegex := regexp.MustCompile(`Model name:\s+(.*)`)
|
||||
coresRegex := regexp.MustCompile(`CPU\(s\):\s+(\d+)`)
|
||||
|
||||
typeMatch := typeRegex.FindStringSubmatch(string(output))
|
||||
if len(typeMatch) > 1 {
|
||||
cpuType = typeMatch[1]
|
||||
}
|
||||
|
||||
coresMatch := coresRegex.FindStringSubmatch(string(output))
|
||||
if len(coresMatch) > 1 {
|
||||
cpuSize = coresMatch[1]
|
||||
}
|
||||
|
||||
if cpuType == "" && cpuSize == "" {
|
||||
return "", "", fmt.Errorf("could not parse CPU information using lscpu")
|
||||
}
|
||||
|
||||
return cpuType, cpuSize, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *Machine) GetRAMSize() (string, error) {
|
||||
output, err := exec.Command("free", "-h", "--si").Output() // Using -h for human-readable and --si for base-10 units
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get RAM information: %w", err)
|
||||
}
|
||||
|
||||
ramSize := ""
|
||||
|
||||
ramRegex := regexp.MustCompile(`Mem:\s+(\S+)`) // Matches the total memory size
|
||||
|
||||
ramMatch := ramRegex.FindStringSubmatch(string(output))
|
||||
if len(ramMatch) > 1 {
|
||||
ramSize = ramMatch[1]
|
||||
} else {
|
||||
return "", fmt.Errorf("could not parse RAM information from free command")
|
||||
}
|
||||
|
||||
return ramSize, nil
|
||||
}
|
||||
|
||||
// func cleanString(s string) string {
|
||||
// s = strings.ToLower(s)
|
||||
|
||||
// reg := regexp.MustCompile("[^a-z0-9]+") // Matches one or more non-alphanumeric characters
|
||||
// return reg.ReplaceAllString(s, "")
|
||||
// }
|
||||
@@ -0,0 +1,112 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nestrilabs/cli/internal/machine"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Initial retry delay
|
||||
initialRetryDelay = 1 * time.Second
|
||||
// Maximum retry delay
|
||||
maxRetryDelay = 30 * time.Second
|
||||
// Factor to increase delay by after each attempt
|
||||
backoffFactor = 2
|
||||
)
|
||||
|
||||
type Party struct {
|
||||
// Channel to signal shutdown
|
||||
done chan struct{}
|
||||
fingerprint string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func NewParty() *Party {
|
||||
m := machine.NewMachine()
|
||||
fingerpint := m.GetMachineID()
|
||||
return &Party{
|
||||
done: make(chan struct{}),
|
||||
fingerprint: fingerpint,
|
||||
hostname: m.Hostname,
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully closes the connection
|
||||
func (p *Party) Shutdown() {
|
||||
close(p.done)
|
||||
}
|
||||
|
||||
func (p *Party) Connect() {
|
||||
baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", p.fingerprint)
|
||||
params := url.Values{}
|
||||
params.Add("_pk", p.hostname)
|
||||
wsURL := baseURL + "?" + params.Encode()
|
||||
|
||||
retryDelay := initialRetryDelay
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.done:
|
||||
log.Info("Shutting down connection")
|
||||
return
|
||||
default:
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to connect to party server", "err", err)
|
||||
time.Sleep(retryDelay)
|
||||
// Increase retry delay exponentially, but cap it
|
||||
retryDelay = time.Duration(float64(retryDelay) * backoffFactor)
|
||||
if retryDelay > maxRetryDelay {
|
||||
retryDelay = maxRetryDelay
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Reset retry delay on successful connection
|
||||
retryDelay = initialRetryDelay
|
||||
|
||||
// Handle connection in a separate goroutine
|
||||
connectionClosed := make(chan struct{})
|
||||
go func() {
|
||||
defer close(connectionClosed)
|
||||
defer conn.Close()
|
||||
|
||||
// Send initial message
|
||||
if err := conn.WriteMessage(websocket.TextMessage, []byte("hello there")); err != nil {
|
||||
log.Error("Failed to send initial message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read messages loop
|
||||
for {
|
||||
select {
|
||||
case <-p.done:
|
||||
return
|
||||
default:
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Error("Error reading message", "err", err)
|
||||
return
|
||||
}
|
||||
log.Info("Received message from party server", "message", string(message))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for either connection to close or shutdown signal
|
||||
select {
|
||||
case <-connectionClosed:
|
||||
log.Warn("Connection closed, attempting to reconnect...")
|
||||
time.Sleep(retryDelay)
|
||||
case <-p.done:
|
||||
log.Info("Shutting down connection")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nestrilabs/cli/internal/machine"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// RetryConfig holds configuration for retry behavior
|
||||
type RetryConfig struct {
|
||||
InitialDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
BackoffFactor float64
|
||||
MaxAttempts int // use 0 for infinite retries
|
||||
}
|
||||
|
||||
// DefaultRetryConfig provides sensible default values
|
||||
var DefaultRetryConfig = RetryConfig{
|
||||
InitialDelay: time.Second,
|
||||
MaxDelay: 30 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
MaxAttempts: 0, // infinite retries
|
||||
}
|
||||
|
||||
// RetryFunc is a function that will be retried
|
||||
type RetryFunc[T any] func() (T, error)
|
||||
|
||||
// Retry executes the given function with retries based on the config
|
||||
func Retry[T any](config RetryConfig, operation RetryFunc[T]) (T, error) {
|
||||
var result T
|
||||
currentDelay := config.InitialDelay
|
||||
attempts := 0
|
||||
|
||||
for {
|
||||
if config.MaxAttempts > 0 && attempts >= config.MaxAttempts {
|
||||
return result, fmt.Errorf("max retry attempts (%d) exceeded", config.MaxAttempts)
|
||||
}
|
||||
|
||||
result, err := operation()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
log.Warn("Operation failed, retrying...",
|
||||
"attempt", attempts+1,
|
||||
"delay", currentDelay,
|
||||
"error", err)
|
||||
|
||||
time.Sleep(currentDelay)
|
||||
|
||||
// Increase delay for next attempt
|
||||
currentDelay = time.Duration(float64(currentDelay) * config.BackoffFactor)
|
||||
if currentDelay > config.MaxDelay {
|
||||
currentDelay = config.MaxDelay
|
||||
}
|
||||
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
|
||||
// MessageHandler processes a message and returns true if it's the expected type
|
||||
type MessageHandler[T any] func(msg T) bool
|
||||
|
||||
type TypeListener[T any] struct {
|
||||
retryConfig RetryConfig
|
||||
handler MessageHandler[T]
|
||||
fingerprint string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func NewTypeListener[T any](handler MessageHandler[T]) *TypeListener[T] {
|
||||
m := machine.NewMachine()
|
||||
fingerprint := m.GetMachineID()
|
||||
|
||||
return &TypeListener[T]{
|
||||
retryConfig: DefaultRetryConfig,
|
||||
handler: handler,
|
||||
fingerprint: fingerprint,
|
||||
hostname: m.Hostname,
|
||||
}
|
||||
}
|
||||
|
||||
// SetRetryConfig allows customizing the retry behavior
|
||||
func (t *TypeListener[T]) SetRetryConfig(config RetryConfig) {
|
||||
t.retryConfig = config
|
||||
}
|
||||
|
||||
func (t *TypeListener[T]) ConnectUntilMessage() (T, error) {
|
||||
baseURL := fmt.Sprintf("ws://localhost:1999/parties/main/%s", t.fingerprint)
|
||||
params := url.Values{}
|
||||
params.Add("_pk", t.hostname)
|
||||
wsURL := baseURL + "?" + params.Encode()
|
||||
|
||||
return Retry(t.retryConfig, func() (T, error) {
|
||||
var result T
|
||||
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("connection failed: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read messages until we get the one we want
|
||||
for {
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("read error: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(message, &result); err != nil {
|
||||
// log.Error("Failed to unmarshal message", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if t.handler(result) {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type resource struct {
|
||||
Api struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
Auth struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
AuthFingerprintKey struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
}
|
||||
|
||||
var Resource resource
|
||||
|
||||
func init() {
|
||||
val := reflect.ValueOf(&Resource).Elem()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
typeField := val.Type().Field(i)
|
||||
envVarName := fmt.Sprintf("SST_RESOURCE_%s", typeField.Name)
|
||||
envValue, exists := os.LookupEnv(envVarName)
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("Environment variable %s is required", envVarName))
|
||||
}
|
||||
if err := json.Unmarshal([]byte(envValue), field.Addr().Interface()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// GPUType represents the type of GPU available
|
||||
type GPUType int
|
||||
|
||||
const (
|
||||
GPUNone GPUType = iota
|
||||
GPUNvidia
|
||||
GPUIntelAMD
|
||||
)
|
||||
|
||||
// Session represents a Docker container session
|
||||
type Session struct {
|
||||
client *client.Client
|
||||
containerID string
|
||||
imageName string
|
||||
config *SessionConfig
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// SessionConfig holds the configuration for the session
|
||||
type SessionConfig struct {
|
||||
Room string
|
||||
Resolution string
|
||||
Framerate string
|
||||
RelayURL string
|
||||
Params string
|
||||
GamePath string
|
||||
}
|
||||
|
||||
// NewSession creates a new Docker session
|
||||
func NewSession(config *SessionConfig) (*Session, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Docker client: %v", err)
|
||||
}
|
||||
|
||||
return &Session{
|
||||
client: cli,
|
||||
imageName: "archlinux", //"ghcr.io/datcaptainhorse/nestri-cachyos:latest-noavx2",
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start initiates the Docker container session
|
||||
func (s *Session) Start(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.isRunning {
|
||||
return fmt.Errorf("session is already running")
|
||||
}
|
||||
|
||||
// Detect GPU type
|
||||
gpuType := detectGPU()
|
||||
if gpuType == GPUNone {
|
||||
return fmt.Errorf("no supported GPU detected")
|
||||
}
|
||||
|
||||
// Get GPU-specific configurations
|
||||
deviceRequests, err := getGPUDeviceRequests(gpuType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
devices := getGPUDevices(gpuType)
|
||||
|
||||
// Check if image exists locally
|
||||
_, _, err = s.client.ImageInspectWithRaw(ctx, s.imageName)
|
||||
if err != nil {
|
||||
// Pull the image if it doesn't exist
|
||||
reader, err := s.client.ImagePull(ctx, s.imageName, image.PullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull image: %v", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Copy pull output to stdout
|
||||
io.Copy(os.Stdout, reader)
|
||||
}
|
||||
|
||||
// Create container
|
||||
resp, err := s.client.ContainerCreate(ctx, &container.Config{
|
||||
Image: s.imageName,
|
||||
Env: []string{
|
||||
fmt.Sprintf("NESTRI_ROOM=%s", s.config.Room),
|
||||
fmt.Sprintf("RESOLUTION=%s", s.config.Resolution),
|
||||
fmt.Sprintf("NESTRI_PARAMS=%s", s.config.Params),
|
||||
fmt.Sprintf("FRAMERATE=%s", s.config.Framerate),
|
||||
fmt.Sprintf("RELAY_URL=%s", s.config.RelayURL),
|
||||
},
|
||||
}, &container.HostConfig{
|
||||
Binds: []string{
|
||||
fmt.Sprintf("%s:/home/nestri/.steam/", s.config.GamePath),
|
||||
},
|
||||
Resources: container.Resources{
|
||||
DeviceRequests: deviceRequests,
|
||||
Devices: devices,
|
||||
},
|
||||
SecurityOpt: []string{"label=disable"},
|
||||
ShmSize: 5368709120, // 5GB
|
||||
// ShmSize: 1073741824, // 1GB
|
||||
}, nil, nil, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container: %v", err)
|
||||
}
|
||||
|
||||
// Start container
|
||||
if err := s.client.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to start container: %v", err)
|
||||
}
|
||||
|
||||
// Store container ID and update state
|
||||
s.containerID = resp.ID
|
||||
s.isRunning = true
|
||||
|
||||
// Start logging in a goroutine
|
||||
go s.streamLogs(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the Docker container session
|
||||
func (s *Session) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.isRunning {
|
||||
return fmt.Errorf("session is not running")
|
||||
}
|
||||
|
||||
timeout := 30 // seconds
|
||||
if err := s.client.ContainerStop(ctx, s.containerID, container.StopOptions{Timeout: &timeout}); err != nil {
|
||||
return fmt.Errorf("failed to stop container: %v", err)
|
||||
}
|
||||
|
||||
if err := s.client.ContainerRemove(ctx, s.containerID, container.RemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to remove container: %v", err)
|
||||
}
|
||||
|
||||
s.isRunning = false
|
||||
s.containerID = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns the current state of the session
|
||||
func (s *Session) IsRunning() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// GetContainerID returns the current container ID
|
||||
func (s *Session) GetContainerID() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.containerID
|
||||
}
|
||||
|
||||
// streamLogs streams container logs to stdout
|
||||
func (s *Session) streamLogs(ctx context.Context) {
|
||||
opts := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
}
|
||||
|
||||
logs, err := s.client.ContainerLogs(ctx, s.containerID, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting container logs: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer logs.Close()
|
||||
|
||||
_, err = io.Copy(os.Stdout, logs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error streaming logs: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyEnvironment checks if all expected environment variables are set correctly in the container
|
||||
func (s *Session) VerifyEnvironment(ctx context.Context) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if !s.isRunning {
|
||||
return fmt.Errorf("session is not running")
|
||||
}
|
||||
|
||||
// Get container info to verify it's actually running
|
||||
inspect, err := s.client.ContainerInspect(ctx, s.containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container: %v", err)
|
||||
}
|
||||
|
||||
if !inspect.State.Running {
|
||||
return fmt.Errorf("container is not in running state")
|
||||
}
|
||||
|
||||
// Expected environment variables
|
||||
expectedEnv := map[string]string{
|
||||
"NESTRI_ROOM": s.config.Room,
|
||||
"RESOLUTION": s.config.Resolution,
|
||||
"FRAMERATE": s.config.Framerate,
|
||||
"RELAY_URL": s.config.RelayURL,
|
||||
"NESTRI_PARAMS": s.config.Params,
|
||||
}
|
||||
|
||||
// Get actual environment variables from container
|
||||
containerEnv := make(map[string]string)
|
||||
for _, env := range inspect.Config.Env {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
containerEnv[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Check each expected variable
|
||||
var missingVars []string
|
||||
var mismatchedVars []string
|
||||
|
||||
for key, expectedValue := range expectedEnv {
|
||||
actualValue, exists := containerEnv[key]
|
||||
if !exists {
|
||||
missingVars = append(missingVars, key)
|
||||
} else if actualValue != expectedValue {
|
||||
mismatchedVars = append(mismatchedVars, fmt.Sprintf("%s (expected: %s, got: %s)",
|
||||
key, expectedValue, actualValue))
|
||||
}
|
||||
}
|
||||
|
||||
// Build error message if there are any issues
|
||||
if len(missingVars) > 0 || len(mismatchedVars) > 0 {
|
||||
var errorMsg strings.Builder
|
||||
if len(missingVars) > 0 {
|
||||
errorMsg.WriteString(fmt.Sprintf("Missing environment variables: %s\n",
|
||||
strings.Join(missingVars, ", ")))
|
||||
}
|
||||
if len(mismatchedVars) > 0 {
|
||||
errorMsg.WriteString(fmt.Sprintf("Mismatched environment variables: %s",
|
||||
strings.Join(mismatchedVars, ", ")))
|
||||
}
|
||||
return fmt.Errorf(errorMsg.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEnvironment returns all environment variables in the container
|
||||
func (s *Session) GetEnvironment(ctx context.Context) (map[string]string, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if !s.isRunning {
|
||||
return nil, fmt.Errorf("session is not running")
|
||||
}
|
||||
|
||||
inspect, err := s.client.ContainerInspect(ctx, s.containerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inspect container: %v", err)
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
for _, e := range inspect.Config.Env {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// ExecResult holds the output from a container command
|
||||
type ExecResult struct {
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
}
|
||||
|
||||
func (s *Session) execInContainer(ctx context.Context, cmd []string) (*ExecResult, error) {
|
||||
execConfig := container.ExecOptions{
|
||||
Cmd: cmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
execID, err := s.client.ContainerExecCreate(ctx, s.containerID, execConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
_, err = io.Copy(&outBuf, resp.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inspect, err := s.client.ContainerExecInspect(ctx, execID.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ExecResult{
|
||||
ExitCode: inspect.ExitCode,
|
||||
Stdout: outBuf.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckSteamGames returns the list of installed games in the container
|
||||
func (s *Session) CheckInstalledSteamGames(ctx context.Context) ([]uint64, error) {
|
||||
result, err := s.execInContainer(ctx, []string{
|
||||
"sh", "-c",
|
||||
"find /home/nestri/.steam/steam/steamapps -name '*.acf' -exec grep -H '\"appid\"' {} \\;",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check steam games: %v", err)
|
||||
}
|
||||
|
||||
var gameIDs []uint64
|
||||
for _, line := range strings.Split(result.Stdout, "\n") {
|
||||
if strings.Contains(line, "appid") {
|
||||
var id uint64
|
||||
if _, err := fmt.Sscanf(line, `"appid" "%d"`, &id); err == nil {
|
||||
gameIDs = append(gameIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gameIDs, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// detectGPU checks for available GPU type
|
||||
func detectGPU() GPUType {
|
||||
// First check for NVIDIA
|
||||
cmd := exec.Command("nvidia-smi")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return GPUNvidia
|
||||
}
|
||||
|
||||
// Check for Intel/AMD GPU by looking for DRI devices
|
||||
if _, err := os.Stat("/dev/dri"); err == nil {
|
||||
return GPUIntelAMD
|
||||
}
|
||||
|
||||
return GPUNone
|
||||
}
|
||||
|
||||
// getGPUDeviceRequests returns appropriate device configuration based on GPU type
|
||||
func getGPUDeviceRequests(gpuType GPUType) ([]container.DeviceRequest, error) {
|
||||
switch gpuType {
|
||||
case GPUNvidia:
|
||||
return []container.DeviceRequest{
|
||||
{
|
||||
Driver: "nvidia",
|
||||
Count: 1,
|
||||
DeviceIDs: []string{"0"},
|
||||
Capabilities: [][]string{{"gpu"}},
|
||||
},
|
||||
}, nil
|
||||
case GPUIntelAMD:
|
||||
return []container.DeviceRequest{}, nil // Empty as we'll handle this in Devices
|
||||
default:
|
||||
return nil, fmt.Errorf("no supported GPU detected")
|
||||
}
|
||||
}
|
||||
|
||||
// getGPUDevices returns appropriate device mappings based on GPU type
|
||||
func getGPUDevices(gpuType GPUType) []container.DeviceMapping {
|
||||
if gpuType == GPUIntelAMD {
|
||||
devices := []container.DeviceMapping{}
|
||||
// Only look for card and renderD nodes
|
||||
for _, pattern := range []string{"card[0-9]*", "renderD[0-9]*"} {
|
||||
matches, err := filepath.Glob(fmt.Sprintf("/dev/dri/%s", pattern))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
// Verify it's a device file
|
||||
if info, err := os.Stat(match); err == nil && (info.Mode()&os.ModeDevice) != 0 {
|
||||
devices = append(devices, container.DeviceMapping{
|
||||
PathOnHost: match,
|
||||
PathInContainer: match,
|
||||
CgroupPermissions: "rwm",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return devices
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"nestrilabs/cli/internal/session"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// err := cmd.Execute()
|
||||
// if err != nil {
|
||||
// log.Error("Error running the cmd command", "err", err)
|
||||
// }
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
config := &session.SessionConfig{
|
||||
Room: "victortest",
|
||||
Resolution: "1920x1080",
|
||||
Framerate: "60",
|
||||
RelayURL: "https://relay.dathorse.com",
|
||||
Params: "--verbose=true --video-codec=h264 --video-bitrate=4000 --video-bitrate-max=6000 --gpu-card-path=/dev/dri/card1",
|
||||
GamePath: "/path/to/your/game",
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(config)
|
||||
if err != nil {
|
||||
log.Error("Failed to create session", "err", err)
|
||||
}
|
||||
|
||||
// Start the session
|
||||
if err := sess.Start(ctx); err != nil {
|
||||
log.Error("Failed to start session", "err", err)
|
||||
}
|
||||
|
||||
// Check if it's running
|
||||
if sess.IsRunning() {
|
||||
log.Info("Session is running with container ID", "containerId", sess.GetContainerID())
|
||||
}
|
||||
|
||||
env, err := sess.GetEnvironment(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get environment: %v", err)
|
||||
} else {
|
||||
for key, value := range env {
|
||||
log.Info("Found this environment variables", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Let it run for a while
|
||||
// time.Sleep(time.Second * 50)
|
||||
|
||||
// Stop the session
|
||||
if err := sess.Stop(ctx); err != nil {
|
||||
log.Error("Failed to stop session", "err", err)
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// image-brightness-analyzer.js
|
||||
|
||||
export class ImageBrightnessAnalyzer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D ;
|
||||
|
||||
constructor() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
analyze(imgElement: HTMLImageElement) {
|
||||
if (!(imgElement instanceof HTMLImageElement)) {
|
||||
throw new Error('Input must be an HTMLImageElement');
|
||||
}
|
||||
|
||||
this.canvas.width = imgElement.width;
|
||||
this.canvas.height = imgElement.height;
|
||||
this.ctx.drawImage(imgElement, 0, 0);
|
||||
|
||||
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let brightestPixel = { value: 0, x: 0, y: 0 };
|
||||
let dullestPixel = { value: 765, x: 0, y: 0 }; // 765 is the max value (255 * 3)
|
||||
|
||||
for (let y = 0; y < this.canvas.height; y++) {
|
||||
for (let x = 0; x < this.canvas.width; x++) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
const brightness = data[index] + data[index + 1] + data[index + 2];
|
||||
|
||||
if (brightness > brightestPixel.value) {
|
||||
brightestPixel = { value: brightness, x, y };
|
||||
}
|
||||
if (brightness < dullestPixel.value) {
|
||||
dullestPixel = { value: brightness, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
brightest: {
|
||||
x: brightestPixel.x,
|
||||
y: brightestPixel.y,
|
||||
color: this.getPixelColor(data, brightestPixel.x, brightestPixel.y)
|
||||
},
|
||||
dullest: {
|
||||
x: dullestPixel.x,
|
||||
y: dullestPixel.y,
|
||||
color: this.getPixelColor(data, dullestPixel.x, dullestPixel.y)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getPixelColor(data: any[] | Uint8ClampedArray, x: number, y: number) {
|
||||
const index = (y * this.canvas.width + x) * 4;
|
||||
return {
|
||||
r: data[index],
|
||||
g: data[index + 1],
|
||||
b: data[index + 2]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// // Export the class for use in browser environments
|
||||
// if (typeof window !== 'undefined') {
|
||||
// window.ImageBrightnessAnalyzer = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
|
||||
// // Export for module environments (if using a bundler)
|
||||
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
// module.exports = ImageBrightnessAnalyzer;
|
||||
// }
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./image-brightness-analyzer.ts"
|
||||
@@ -0,0 +1,35 @@
|
||||
// Docs: https://www.instantdb.com/docs/permissions
|
||||
|
||||
import type { InstantRules } from "@instantdb/core";
|
||||
|
||||
const rules = {
|
||||
/**
|
||||
* Welcome to Instant's permission system!
|
||||
* Right now your rules are empty. To start filling them in, check out the docs:
|
||||
* https://www.instantdb.com/docs/permissions
|
||||
*
|
||||
* Here's an example to give you a feel:
|
||||
* posts: {
|
||||
* allow: {
|
||||
* view: "true",
|
||||
* create: "isOwner",
|
||||
* update: "isOwner",
|
||||
* delete: "isOwner",
|
||||
* },
|
||||
* bind: ["isOwner", "auth.id != null && auth.id == data.ownerId"],
|
||||
* },
|
||||
*/
|
||||
"$default": {
|
||||
"allow": {
|
||||
"$default": "false"
|
||||
}
|
||||
},
|
||||
machines: {
|
||||
allow: {
|
||||
"$default": "isOwner",
|
||||
},
|
||||
bind: ["isOwner", "auth.id != null && auth.id == data.ownerID"],
|
||||
}
|
||||
} satisfies InstantRules;
|
||||
|
||||
export default rules;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { i } from "@instantdb/core";
|
||||
|
||||
const _schema = i.schema({
|
||||
// This section lets you define entities: think `posts`, `comments`, etc
|
||||
// Take a look at the docs to learn more:
|
||||
// https://www.instantdb.com/docs/modeling-data#2-attributes
|
||||
entities: {
|
||||
$users: i.entity({
|
||||
email: i.string().unique().indexed(),
|
||||
}),
|
||||
// This is here because the $users entity has no more than 1 property; email
|
||||
// profiles: i.entity({
|
||||
// name: i.string(),
|
||||
// location: i.string(),
|
||||
// createdAt: i.date(),
|
||||
// deletedAt: i.date().optional()
|
||||
// }),
|
||||
machines: i.entity({
|
||||
hostname: i.string(),
|
||||
location: i.string(),
|
||||
fingerprint: i.string().indexed(),
|
||||
createdAt: i.date(),
|
||||
deletedAt: i.date().optional().indexed()
|
||||
}),
|
||||
// teams: i.entity({
|
||||
// name: i.string(),
|
||||
// type: i.string(), // "Personal" or "Family"
|
||||
// createdAt: i.date(),
|
||||
// deletedAt: i.date().optional()
|
||||
// }),
|
||||
// subscriptions: i.entity({
|
||||
// quantity: i.number(),
|
||||
// polarOrderID: i.string(),
|
||||
// frequency: i.string(),
|
||||
// next: i.date().optional(),
|
||||
// }),
|
||||
// productVariants: i.entity({
|
||||
// name: i.string(),
|
||||
// price: i.number()
|
||||
// })
|
||||
},
|
||||
// links: {
|
||||
// userProfiles: {
|
||||
// forward: { on: 'profiles', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'one', label: 'profile' },
|
||||
// },
|
||||
// machineOwners: {
|
||||
// forward: { on: 'machines', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'machinesOwned' },
|
||||
// },
|
||||
// machineTeams: {
|
||||
// forward: { on: 'machines', has: 'one', label: 'team' },
|
||||
// reverse: { on: 'teams', has: 'many', label: 'machines' },
|
||||
// },
|
||||
// userTeams: {
|
||||
// forward: { on: 'teams', has: 'one', label: 'owner' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'teamsOwned' },
|
||||
// },
|
||||
// teamMembers: {
|
||||
// forward: { on: 'teams', has: 'many', label: 'members' },
|
||||
// reverse: { on: '$users', has: 'many', label: 'teams' },
|
||||
// },
|
||||
// subscribedProduct: {
|
||||
// forward: { on: "subscriptions", has: "one", label: "productVariant" },
|
||||
// reverse: { on: "productVariants", has: "many", label: "subscriptions" }
|
||||
// },
|
||||
// subscribedUser: {
|
||||
// forward: { on: "subscriptions", has: "one", label: "owner" },
|
||||
// reverse: { on: "$users", has: "many", label: "subscriptions" }
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
// This helps Typescript display nicer intellisense
|
||||
type _AppSchema = typeof _schema;
|
||||
interface AppSchema extends _AppSchema { }
|
||||
const schema: AppSchema = _schema;
|
||||
|
||||
export type { AppSchema };
|
||||
export default schema;
|
||||
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"name": "@nestri/core",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"exports":{
|
||||
".":"./index.ts"
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240529.0",
|
||||
"wrangler": "^3.57.2"
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"loops": "^3.4.1",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^11.0.3",
|
||||
"zod": "^3.24.1",
|
||||
"zod-openapi": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@instantdb/admin": "^0.17.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createContext } from "./context";
|
||||
import { VisibleError } from "./error";
|
||||
|
||||
export interface UserActor {
|
||||
type: "user";
|
||||
properties: {
|
||||
accessToken: string;
|
||||
userID: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeviceActor {
|
||||
type: "device";
|
||||
properties: {
|
||||
fingerprint: string;
|
||||
id: string;
|
||||
auth?:
|
||||
| {
|
||||
type: "personal";
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "oauth";
|
||||
clientID: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface PublicActor {
|
||||
type: "public";
|
||||
properties: {};
|
||||
}
|
||||
|
||||
type Actor = UserActor | PublicActor | DeviceActor;
|
||||
export const ActorContext = createContext<Actor>();
|
||||
|
||||
export function useCurrentUser() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "user") return {
|
||||
id:actor.properties.userID,
|
||||
token: actor.properties.accessToken
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useCurrentDevice() {
|
||||
const actor = ActorContext.use();
|
||||
if (actor.type === "device") return {
|
||||
fingerprint:actor.properties.fingerprint,
|
||||
id: actor.properties.id
|
||||
};
|
||||
throw new VisibleError(
|
||||
"auth",
|
||||
"unauthorized",
|
||||
`You don't have permission to access this resource`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useActor() {
|
||||
try {
|
||||
return ActorContext.use();
|
||||
} catch {
|
||||
return { type: "public", properties: {} } as PublicActor;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertActor<T extends Actor["type"]>(type: T) {
|
||||
const actor = useActor();
|
||||
if (actor.type !== type)
|
||||
throw new VisibleError("auth", "actor.invalid", `Actor is not "${type}"`);
|
||||
return actor as Extract<Actor, { type: T }>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import "zod-openapi/extend";
|
||||
|
||||
export module Common {
|
||||
export const IdDescription = `Unique object identifier.
|
||||
The format and length of IDs may change over time.`;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
export function createContext<T>() {
|
||||
const storage = new AsyncLocalStorage<T>();
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore();
|
||||
if (!result) {
|
||||
throw new Error("No context available");
|
||||
}
|
||||
return result;
|
||||
},
|
||||
with<R>(value: T, fn: () => R) {
|
||||
return storage.run<R>(value, fn);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Resource } from "sst";
|
||||
import { init } from "@instantdb/admin";
|
||||
import schema from "../instant.schema";
|
||||
|
||||
const databaseClient = () => init({
|
||||
appId: Resource.InstantAppId.value,
|
||||
adminToken: Resource.InstantAdminToken.value,
|
||||
schema
|
||||
})
|
||||
|
||||
|
||||
export default databaseClient
|
||||
@@ -0,0 +1,25 @@
|
||||
import { LoopsClient } from "loops";
|
||||
import { Resource } from "sst/resource"
|
||||
export namespace Email {
|
||||
export const Client = () => new LoopsClient(Resource.LoopsApiKey.value);
|
||||
|
||||
export async function send(
|
||||
to: string,
|
||||
body: string,
|
||||
) {
|
||||
|
||||
try {
|
||||
await Client().sendTransactionalEmail(
|
||||
{
|
||||
transactionalId: "cm58pdf8d03upb5ecirnmvrfb",
|
||||
email: to,
|
||||
dataVariables: {
|
||||
logincode: body
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error sending email", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class VisibleError extends Error {
|
||||
constructor(
|
||||
public kind: "input" | "auth",
|
||||
public code: string,
|
||||
public message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export module Examples {
|
||||
|
||||
export const User = {
|
||||
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
|
||||
email: "john@example.com",
|
||||
};
|
||||
|
||||
export const Machine = {
|
||||
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||
hostname: "desktopeuo8vsf",
|
||||
fingerprint: "fc27f428f9ca47d4b41b70889ae0c62090",
|
||||
location: "KE, AF"
|
||||
}
|
||||
|
||||
// export const Team = {
|
||||
// id: createID(),
|
||||
// name: "Jane's Family",
|
||||
// type: "Family"
|
||||
// }
|
||||
|
||||
// export const ProductVariant = {
|
||||
// id: createID(),
|
||||
// name: "FamilySM",
|
||||
// price: 10,
|
||||
// };
|
||||
|
||||
// export const Product = {
|
||||
// id: createID(),
|
||||
// name: "Family",
|
||||
// description: "The ideal subscription tier for dedicated gamers who crave more flexibility and social gaming experiences.",
|
||||
// variants: [ProductVariant],
|
||||
// subscription: "allowed" as const,
|
||||
// };
|
||||
|
||||
// export const Subscription = {
|
||||
// id: createID(),
|
||||
// productVariant: ProductVariant,
|
||||
// quantity: 1,
|
||||
// polarOrderID: createID(),
|
||||
// frequency: "monthly" as const,
|
||||
// next: new Date("2024-02-01 19:36:19.000").getTime(),
|
||||
// owner: User
|
||||
// };
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { z } from "zod"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import databaseClient from "../database"
|
||||
import { useCurrentUser } from "../actor";
|
||||
import { id as createID } from "@instantdb/admin";
|
||||
|
||||
export module Machine {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Machine.id,
|
||||
}),
|
||||
hostname: z.string().openapi({
|
||||
description: "Hostname of the machine",
|
||||
example: Examples.Machine.hostname,
|
||||
}),
|
||||
fingerprint: z.string().openapi({
|
||||
description: "The machine's fingerprint, derived from the machine's Linux machine ID.",
|
||||
example: Examples.Machine.fingerprint,
|
||||
}),
|
||||
location: z.string().openapi({
|
||||
description: "The machine's approximate location; country and continent.",
|
||||
example: Examples.Machine.location,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Machine",
|
||||
description: "A machine running on the Nestri network.",
|
||||
example: Examples.Machine,
|
||||
});
|
||||
|
||||
export const create = fn(z.object({
|
||||
fingerprint: z.string(),
|
||||
hostname: z.string(),
|
||||
location: z.string()
|
||||
}), async (input) => {
|
||||
const id = createID()
|
||||
const now = new Date().getTime()
|
||||
const db = databaseClient()
|
||||
await db.transact(
|
||||
db.tx.machines[id]!.update({
|
||||
fingerprint: input.fingerprint,
|
||||
hostname: input.hostname,
|
||||
location: input.location,
|
||||
createdAt: now,
|
||||
})
|
||||
)
|
||||
|
||||
return id
|
||||
})
|
||||
|
||||
export const remove = fn(z.string(), async (id) => {
|
||||
const now = new Date().getTime()
|
||||
// const device = useCurrentDevice()
|
||||
// const db = databaseClient()
|
||||
|
||||
// if (device.id) { // the machine can delete itself
|
||||
// await db.transact(db.tx.machines[device.id]!.update({ deletedAt: now }))
|
||||
// } else {// the user can delete it manually
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
await db.transact(db.tx.machines[id]!.update({ deletedAt: now }))
|
||||
// }
|
||||
|
||||
return "ok"
|
||||
})
|
||||
|
||||
export const fromID = fn(z.string(), async (id) => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
|
||||
const query = {
|
||||
machines: {
|
||||
$: {
|
||||
where: {
|
||||
id: id,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.machines[0]
|
||||
})
|
||||
|
||||
export const fromFingerprint = fn(z.string(), async (input) => {
|
||||
const db = databaseClient()
|
||||
|
||||
const query = {
|
||||
machines: {
|
||||
$: {
|
||||
where: {
|
||||
fingerprint: input,
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.machines[0]
|
||||
})
|
||||
|
||||
export const list = async () => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient().asUser({ token: user.token })
|
||||
|
||||
const query = {
|
||||
$users: {
|
||||
$: { where: { id: user.id } },
|
||||
machines: {
|
||||
$: {
|
||||
deletedAt: { $isNull: true }
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const res = await db.query(query)
|
||||
|
||||
return res.$users[0]?.machines
|
||||
}
|
||||
|
||||
export const link = fn(z.object({
|
||||
machineId: z.string()
|
||||
}), async (input) => {
|
||||
const user = useCurrentUser()
|
||||
const db = databaseClient()
|
||||
|
||||
await db.transact(db.tx.machines[input.machineId]!.link({ owner: user.id }))
|
||||
|
||||
return "ok"
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import databaseClient from "../database"
|
||||
import { z } from "zod"
|
||||
import { Common } from "../common";
|
||||
import { createID, fn } from "../utils";
|
||||
import { Examples } from "../examples";
|
||||
|
||||
export module Team {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Team.id,
|
||||
}),
|
||||
name: z.string().openapi({
|
||||
description: "Name of the machine",
|
||||
example: Examples.Team.name,
|
||||
}),
|
||||
type: z.string().nullable().openapi({
|
||||
description: "Whether this is a personal or family type of team",
|
||||
example: Examples.Team.type,
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Team",
|
||||
description: "A group of Nestri user's who share the same machine",
|
||||
example: Examples.Team,
|
||||
});
|
||||
|
||||
export const create = fn(z.object({
|
||||
name: z.string(),
|
||||
type: z.enum(["personal", "family"]),
|
||||
owner: z.string(),
|
||||
}), async (input) => {
|
||||
const id = createID("machine")
|
||||
const now = new Date().getTime()
|
||||
const db = databaseClient()
|
||||
|
||||
await db.transact(db.tx.teams[id]!.update({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
createdAt: now
|
||||
}).link({
|
||||
owner: input.owner,
|
||||
}))
|
||||
|
||||
return id
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface CloudflareCF {
|
||||
colo: string;
|
||||
continent: string;
|
||||
country: string,
|
||||
city: string;
|
||||
region: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
metroCode: string;
|
||||
postalCode: string;
|
||||
timezone: string;
|
||||
regionCode: number;
|
||||
}
|
||||
|
||||
export interface CFRequest extends Request {
|
||||
cf: CloudflareCF
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import databaseClient from "../database"
|
||||
import { fn } from "../utils";
|
||||
import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
|
||||
export module User {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.User.id,
|
||||
}),
|
||||
email: z.string().nullable().openapi({
|
||||
description: "Email address of the user.",
|
||||
example: Examples.User.email,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
ref: "User",
|
||||
description: "A Nestri console user.",
|
||||
example: Examples.User,
|
||||
});
|
||||
|
||||
export const fromEmail = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const res = await db.auth.getUser({ email })
|
||||
return res
|
||||
})
|
||||
|
||||
export const create = fn(z.string(), async (email) => {
|
||||
const db = databaseClient()
|
||||
const token = await db.auth.createToken(email)
|
||||
|
||||
return token
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ZodSchema, z } from "zod";
|
||||
|
||||
export function fn<
|
||||
Arg1 extends ZodSchema,
|
||||
Callback extends (arg1: z.output<Arg1>) => any,
|
||||
>(arg1: Arg1, cb: Callback) {
|
||||
const result = function (input: z.input<typeof arg1>): ReturnType<Callback> {
|
||||
const parsed = arg1.parse(input);
|
||||
return cb.apply(cb, [parsed as any]);
|
||||
};
|
||||
result.schema = arg1;
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./fn"
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
import "sst"
|
||||
export {}
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"Auth": {
|
||||
"type": "sst.cloudflare.Worker"
|
||||
"url": string
|
||||
}
|
||||
"AuthFingerprintKey": {
|
||||
"type": "random.index/randomString.RandomString"
|
||||
"value": string
|
||||
}
|
||||
"CloudflareAuthKV": {
|
||||
"type": "sst.cloudflare.Kv"
|
||||
}
|
||||
"InstantAdminToken": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"InstantAppId": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"LoopsApiKey": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Urls": {
|
||||
"api": string
|
||||
"auth": string
|
||||
"type": "sst.sst.Linkable"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@tsconfig/node20/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# https://github.com/iximiuz/docker-to-linux/blob/master/Dockerfile
|
||||
|
||||
FROM amd64/debian:bullseye
|
||||
LABEL com.iximiuz-project="docker-to-linux"
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install extlinux fdisk
|
||||
@@ -1,15 +0,0 @@
|
||||
# Docker to RootFS
|
||||
|
||||
We are building the rootfs as a docker image, mainly for consistency and ease of use.
|
||||
So, to convert the Docker image to a rootfs good enough to run inside CrosVM we use this script
|
||||
|
||||
Run it like so:
|
||||
```bash
|
||||
./make your-docker-image
|
||||
```
|
||||
|
||||
TODO:
|
||||
1. Make sure the docker image name passed in exists
|
||||
2. Reduce the dependencies of this script to 1 (If possible)
|
||||
3. Extract not only the rootfs, but also kernel and initrd
|
||||
4. Add a way to pass in the size of the rootfs the user wants
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
UID_HOST=$1
|
||||
GID_HOST=$2
|
||||
VM_DISK_SIZE_MB=$3
|
||||
|
||||
echo_blue() {
|
||||
local font_blue="\033[94m"
|
||||
local font_bold="\033[1m"
|
||||
local font_end="\033[0m"
|
||||
|
||||
echo -e "\n${font_blue}${font_bold}${1}${font_end}"
|
||||
}
|
||||
|
||||
echo_blue "[Create disk image]"
|
||||
[ -z "${VM_DISK_SIZE_MB}" ] && VM_DISK_SIZE_MB=1024
|
||||
VM_DISK_SIZE_SECTOR=$(expr $VM_DISK_SIZE_MB \* 1024 \* 1024 / 512)
|
||||
dd if=/dev/zero of=/os/${DISTR}.img bs=${VM_DISK_SIZE_SECTOR} count=512
|
||||
|
||||
echo_blue "[Make partition]"
|
||||
echo "type=83,bootable" | sfdisk /os/${DISTR}.img
|
||||
|
||||
echo_blue "\n[Format partition with ext4]"
|
||||
losetup -D
|
||||
LOOPDEVICE=$(losetup -f)
|
||||
echo -e "\n[Using ${LOOPDEVICE} loop device]"
|
||||
losetup -o $(expr 512 \* 2048) ${LOOPDEVICE} /os/${DISTR}.img
|
||||
mkfs.ext4 ${LOOPDEVICE}
|
||||
|
||||
echo_blue "[Copy ${DISTR} directory structure to partition]"
|
||||
mkdir -p /os/mnt
|
||||
mount -t auto ${LOOPDEVICE} /os/mnt/
|
||||
cp -a /os/${DISTR}.dir/. /os/mnt/
|
||||
|
||||
echo_blue "[Setup extlinux]"
|
||||
extlinux --install /os/mnt/boot/
|
||||
cp /os/syslinux.cfg /os/mnt/boot/syslinux.cfg
|
||||
rm /os/mnt/.dockerenv
|
||||
|
||||
echo_blue "[Unmount]"
|
||||
umount /os/mnt
|
||||
losetup -D
|
||||
|
||||
echo_blue "[Write syslinux MBR]"
|
||||
dd if=/usr/lib/syslinux/mbr/mbr.bin of=/os/${DISTR}.img bs=440 count=1 conv=notrunc
|
||||
|
||||
#echo_blue "[Convert to qcow2]"
|
||||
#qemu-img convert -c /os/${DISTR}.img -O qcow2 /os/${DISTR}.qcow2
|
||||
|
||||
[ "${UID_HOST}" -a "${GID_HOST}" ] && chown ${UID_HOST}:${GID_HOST} /os/${DISTR}.img
|
||||
|
||||
rm -r /os/${DISTR}.dir /os/${DISTR}.tar
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define colors
|
||||
COL_RED="\033[0;31m"
|
||||
COL_GRN="\033[0;32m"
|
||||
COL_END="\033[0m"
|
||||
|
||||
# Get current user and group IDs
|
||||
USER_ID=$(id -u)
|
||||
GROUP_ID=$(id -g)
|
||||
|
||||
# Set default disk size
|
||||
VM_DISK_SIZE_MB=${VM_DISK_SIZE_MB:-4096}
|
||||
|
||||
# Set repository name
|
||||
REPO="nestri"
|
||||
|
||||
# Function to create a tar archive from a Docker image
|
||||
create_tar() {
|
||||
local name=$1
|
||||
echo -e "\n${COL_GRN}[Dump $name directory structure to tar archive]${COL_END}"
|
||||
docker export -o $name.tar $(docker run -d $name /bin/true)
|
||||
}
|
||||
|
||||
# Function to extract a tar archive
|
||||
extract_tar() {
|
||||
local name=$1
|
||||
echo -e "\n${COL_GRN}[Extract $name tar archive]${COL_END}"
|
||||
docker run -it \
|
||||
-v $(pwd):/os:rw \
|
||||
$REPO/builder bash -c "mkdir -p /os/$name.dir && tar -C /os/$name.dir --numeric-owner -xf /os/$name.tar"
|
||||
}
|
||||
|
||||
# Function to create a disk image
|
||||
create_image() {
|
||||
local name=$1
|
||||
echo -e "\n${COL_GRN}[Create $name disk image]${COL_END}"
|
||||
docker run -it \
|
||||
-v $(pwd):/os:rw \
|
||||
-e DISTR=$name \
|
||||
--privileged \
|
||||
--cap-add SYS_ADMIN \
|
||||
$REPO/builder bash /os/create_image.sh $USER_ID $GROUP_ID $VM_DISK_SIZE_MB
|
||||
}
|
||||
|
||||
# Function to ensure builder is ready
|
||||
ensure_builder() {
|
||||
echo -e "\n${COL_GRN}[Ensure builder is ready]${COL_END}"
|
||||
if [ "$(docker images -q $REPO/builder)" = '' ]; then
|
||||
docker build -f Dockerfile -t $REPO/builder .
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to run builder interactively
|
||||
run_builder_interactive() {
|
||||
docker run -it \
|
||||
-v $(pwd):/os:rw \
|
||||
--cap-add SYS_ADMIN \
|
||||
$REPO/builder bash
|
||||
}
|
||||
|
||||
# Function to clean up
|
||||
clean_up() {
|
||||
echo -e "\n${COL_GRN}[Remove leftovers]${COL_END}"
|
||||
rm -rf mnt debian.* alpine.* ubuntu.*
|
||||
clean_docker_procs
|
||||
clean_docker_images
|
||||
}
|
||||
|
||||
# Function to clean Docker processes
|
||||
clean_docker_procs() {
|
||||
echo -e "\n${COL_GRN}[Remove Docker Processes]${COL_END}"
|
||||
if [ "$(docker ps -qa -f=label=com.iximiuz-project=$REPO)" != '' ]; then
|
||||
docker rm $(docker ps -qa -f=label=com.iximiuz-project=$REPO)
|
||||
else
|
||||
echo "<noop>"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to clean Docker images
|
||||
clean_docker_images() {
|
||||
echo -e "\n${COL_GRN}[Remove Docker Images]${COL_END}"
|
||||
if [ "$(docker images -q $REPO/*)" != '' ]; then
|
||||
docker rmi $(docker images -q $REPO/*)
|
||||
else
|
||||
echo "<noop>"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main script
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <image_name> [clean|builder-interactive]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_NAME=$1
|
||||
|
||||
case $2 in
|
||||
builder-interactive)
|
||||
run_builder_interactive
|
||||
;;
|
||||
clean)
|
||||
clean_up
|
||||
;;
|
||||
esac
|
||||
|
||||
ensure_builder
|
||||
create_tar $IMAGE_NAME
|
||||
extract_tar $IMAGE_NAME
|
||||
create_image $IMAGE_NAME
|
||||
|
||||
# Extract kernel `` virt-builder --get-kernel "${IMAGE_NAME}.img" -o . ``
|
||||
@@ -1,5 +0,0 @@
|
||||
DEFAULT linux
|
||||
SAY Now booting the kernel from SYSLINUX...
|
||||
LABEL linux
|
||||
KERNEL /vmlinuz
|
||||
APPEND rw root=/dev/sda1 initrd=/initrd.img
|
||||
@@ -1,3 +0,0 @@
|
||||
# `@turbo/eslint-config`
|
||||
|
||||
Collection of internal eslint configurations.
|
||||
@@ -1,34 +0,0 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["eslint:recommended", "prettier", "turbo"],
|
||||
plugins: ["only-warn"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
// Ignore dotfiles
|
||||
".*.js",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js?(x)", "*.ts?(x)"],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"prettier",
|
||||
require.resolve("@vercel/style-guide/eslint/next"),
|
||||
"turbo",
|
||||
],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
},
|
||||
plugins: ["only-warn"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
// Ignore dotfiles
|
||||
".*.js",
|
||||
"node_modules/",
|
||||
],
|
||||
overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }],
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "@nestri/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"library.js",
|
||||
"next.js",
|
||||
"qwik.js",
|
||||
"react-internal.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@vercel/style-guide": "^5.2.0",
|
||||
"eslint-config-turbo": "^2.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"@types/eslint": "8.56.10",
|
||||
"@types/node": "20.14.11",
|
||||
"@typescript-eslint/eslint-plugin": "7.16.1",
|
||||
"@typescript-eslint/parser": "7.16.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-qwik": "^1.8.0",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:qwik/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json"],
|
||||
ecmaVersion: 2021,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// Warn when an unused variable doesn't start with an underscore
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"prefer-spread": "off",
|
||||
"no-case-declarations": "off",
|
||||
"no-console": "off",
|
||||
"qwik/no-use-visible-task": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "warn",
|
||||
"@typescript-eslint/no-unnecessary-condition": "warn",
|
||||
},
|
||||
};
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use with
|
||||
* internal (bundled by their consumer) libraries
|
||||
* that utilize React.
|
||||
*/
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["eslint:recommended", "prettier", "turbo"],
|
||||
plugins: ["only-warn"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
// Ignore dotfiles
|
||||
".*.js",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
],
|
||||
overrides: [
|
||||
// Force ESLint to detect .tsx files
|
||||
{ files: ["*.js?(x)", "*.ts?(x)"] },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user