Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Establish CLI Patterns, Implement Auth to Prepare for Snippets Sync #12

Merged
merged 3 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"noExplicitAny": "off"
},
"complexity": {
"noForEach": "info"
"noForEach": "off"
},
"style": {
"noUselessElse": "off",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
8 changes: 8 additions & 0 deletions cli/CliContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CliConfig } from "lib/cli-config"

/**
* The CLI context contains information that is commonly passed to all functions
*/
export interface CliContext {
cliConfig: CliConfig
}
73 changes: 73 additions & 0 deletions cli/auth/login/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Command } from "commander"
import { cliConfig } from "lib/cli-config"
import delay from "delay"
import { getKy } from "lib/registry-api/get-ky"
import type { EndpointResponse } from "lib/registry-api/endpoint-types"

export const registerAuthLogin = (program: Command) => {
program.commands
.find((c) => c.name() === "auth")!
.command("login")
.description("Authenticate CLI, login to registry")
.action(async (args) => {
const ky = getKy()

const { login_page } = await ky
.post<EndpointResponse["sessions/login_page/create"]>(
"sessions/login_page/create",
{
json: {},
},
)
.json()

console.log("Please visit the following URL to log in:")
console.log(login_page.url)

// Wait until we receive confirmation
while (true) {
const { login_page: new_login_page } = await ky
.post<EndpointResponse["sessions/login_page/get"]>(
"sessions/login_page/get",
{
json: {
login_page_id: login_page.login_page_id,
},
headers: {
Authorization: `Bearer ${login_page.login_page_auth_token}`,
},
},
)
.json()

if (new_login_page.was_login_successful) {
console.log("Logged in! Generating token...")
break
}

if (new_login_page.is_expired) {
throw new Error("Login page expired")
}

await delay(1000)
}

const { session } = await ky
.post<EndpointResponse["sessions/login_page/exchange_for_cli_session"]>(
"sessions/login_page/exchange_for_cli_session",
{
json: {
login_page_id: login_page.login_page_id,
},
headers: {
Authorization: `Bearer ${login_page.login_page_auth_token}`,
},
},
)
.json()

cliConfig.set("sessionToken", session.token)

console.log("Ready to use!")
})
}
11 changes: 11 additions & 0 deletions cli/auth/logout/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Command } from "commander"

export const registerAuthLogout = (program: Command) => {
program.commands
.find((c) => c.name() === "auth")!
.command("logout")
.description("Logout from registry")
.action((args) => {
console.log("logout")
})
}
5 changes: 5 additions & 0 deletions cli/auth/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Command } from "commander"

export const registerAuth = (program: Command) => {
program.command("auth").description("Login/logout")
}
12 changes: 12 additions & 0 deletions cli/config/print/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Command } from "commander"
import { cliConfig } from "lib/cli-config"

export const registerConfigPrint = (program: Command) => {
program.commands
.find((c) => c.name() === "config")!
.command("print")
.description("Print the current config")
.action(() => {
console.log(JSON.stringify(cliConfig.all, null, 2))
})
}
5 changes: 5 additions & 0 deletions cli/config/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Command } from "commander"

export const registerConfig = (program: Command) => {
program.command("config").description("Manage tscircuit CLI configuration")
}
111 changes: 111 additions & 0 deletions cli/dev/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Command } from "commander"
import * as path from "node:path"
import * as chokidar from "chokidar"
import * as fs from "node:fs"
import { createServer } from "lib/server/createServer"
import { getLocalFileDependencies } from "lib/dependency-analysis/getLocalFileDependencies"
import { installTypes } from "../../lib/dependency-analysis/installNodeModuleTypes"
import { EventsWatcher } from "../../lib/server/EventsWatcher"

export const registerDev = (program: Command) => {
program
.command("dev")
.description("Start development server for a snippet")
.argument("<file>", "Path to the snippet file")
.option("-p, --port <number>", "Port to run server on", "3000")
.action(async (file: string, options: { port: string }) => {
const absolutePath = path.resolve(file)
const fileDir = path.dirname(absolutePath)
const port = parseInt(options.port)

try {
console.log("Installing types for imported snippets...")
await installTypes(absolutePath)
console.log("Types installed successfully")
} catch (error) {
console.warn("Failed to install types:", error)
}

// Start the server
await createServer(port)

const eventsWatcher = new EventsWatcher(`http://localhost:${port}`)
eventsWatcher.start()

await fetch(`http://localhost:${port}/api/files/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_path: "entrypoint.tsx",
text_content: `
import MyCircuit from "./snippet.tsx"

circuit.add(<MyCircuit />)
`,
}),
})

// Function to update file content
const updateFile = async (filePath: string) => {
try {
const content = await fs.promises.readFile(filePath, "utf-8")
const response = await fetch(
`http://localhost:${port}/api/files/upsert`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_path: path.relative(fileDir, filePath),
text_content: content,
}),
},
)
if (!response.ok) {
console.error(`Failed to update ${filePath}`)
}
} catch (error) {
console.error(`Error updating ${filePath}:`, error)
}
}

// Get initial dependencies
const dependencies = new Set([absolutePath])
try {
const deps = getLocalFileDependencies(absolutePath)
deps.forEach((dep) => dependencies.add(dep))
} catch (error) {
console.warn("Failed to analyze dependencies:", error)
}

// Watch the main file and its dependencies
const filesystemWatcher = chokidar.watch(Array.from(dependencies), {
persistent: true,
ignoreInitial: false,
})

filesystemWatcher.on("change", async (filePath) => {
console.log(`File ${filePath} changed`)
await updateFile(filePath)
})

filesystemWatcher.on("add", async (filePath) => {
console.log(`File ${filePath} added`)
await updateFile(filePath)
})

eventsWatcher.on("FILE_UPDATED", async (ev) => {
if (ev.file_path === "manual-edits.json") {
console.log("Manual edits updated, updating on filesystem...")
const { file } = await fetch(
`http://localhost:${port}/api/files/get?file_path=manual-edits.json`,
).then((r) => r.json())
fs.writeFileSync(
path.join(fileDir, "manual-edits.json"),
file.text_content,
)
}
})

console.log(`Watching ${file} and its dependencies...`)
})
}
126 changes: 19 additions & 107 deletions cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,119 +1,31 @@
#!/usr/bin/env node
import { Command } from "commander"
import * as path from "node:path"
import * as chokidar from "chokidar"
import * as fs from "node:fs"
import { createServer } from "../lib/server/createServer"
import { getLocalFileDependencies } from "../lib/dependency-analysis/getLocalFileDependencies"
import { installTypes } from "./installTypes"
import { EventsWatcher } from "../lib/server/EventsWatcher"
import { registerDev } from "./dev/register"
import { registerAuthLogin } from "./auth/login/register"
import { registerAuthLogout } from "./auth/logout/register"
import { registerAuth } from "./auth/register"
import { registerConfig } from "./config/register"
import { registerConfigPrint } from "./config/print/register"
import { perfectCli } from "perfect-cli"

const program = new Command()

program
.name("snippets")
.name("tsci")
.description("CLI for developing tscircuit snippets")
.version("1.0.0")

program
.command("dev")
.description("Start development server for a snippet")
.argument("<file>", "Path to the snippet file")
.option("-p, --port <number>", "Port to run server on", "3000")
.action(async (file: string, options: { port: string }) => {
const absolutePath = path.resolve(file)
const fileDir = path.dirname(absolutePath)
const port = parseInt(options.port)

try {
console.log("Installing types for imported snippets...")
await installTypes(absolutePath)
console.log("Types installed successfully")
} catch (error) {
console.warn("Failed to install types:", error)
}

// Start the server
await createServer(port)

const eventsWatcher = new EventsWatcher(`http://localhost:${port}`)
eventsWatcher.start()

await fetch(`http://localhost:${port}/api/files/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_path: "entrypoint.tsx",
text_content: `
import MyCircuit from "./snippet.tsx"

circuit.add(<MyCircuit />)
`,
}),
})

// Function to update file content
const updateFile = async (filePath: string) => {
try {
const content = await fs.promises.readFile(filePath, "utf-8")
const response = await fetch(
`http://localhost:${port}/api/files/upsert`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_path: path.relative(fileDir, filePath),
text_content: content,
}),
},
)
if (!response.ok) {
console.error(`Failed to update ${filePath}`)
}
} catch (error) {
console.error(`Error updating ${filePath}:`, error)
}
}

// Get initial dependencies
const dependencies = new Set([absolutePath])
try {
const deps = getLocalFileDependencies(absolutePath)
deps.forEach((dep) => dependencies.add(dep))
} catch (error) {
console.warn("Failed to analyze dependencies:", error)
}

// Watch the main file and its dependencies
const filesystemWatcher = chokidar.watch(Array.from(dependencies), {
persistent: true,
ignoreInitial: false,
})

filesystemWatcher.on("change", async (filePath) => {
console.log(`File ${filePath} changed`)
await updateFile(filePath)
})

filesystemWatcher.on("add", async (filePath) => {
console.log(`File ${filePath} added`)
await updateFile(filePath)
})
registerDev(program)

eventsWatcher.on("FILE_UPDATED", async (ev) => {
if (ev.file_path === "manual-edits.json") {
console.log("Manual edits updated, updating on filesystem...")
const { file } = await fetch(
`http://localhost:${port}/api/files/get?file_path=manual-edits.json`,
).then((r) => r.json())
fs.writeFileSync(
path.join(fileDir, "manual-edits.json"),
file.text_content,
)
}
})
registerAuth(program)
registerAuthLogin(program)
registerAuthLogout(program)

console.log(`Watching ${file} and its dependencies...`)
})
registerConfig(program)
registerConfigPrint(program)

program.parse()
if (process.argv.length === 2) {
perfectCli(program, process.argv)
} else {
program.parse()
}
Loading
Loading