From 3e64ce805c443fbd888c9639ab1de669c376b198 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 5 Jan 2025 23:49:15 -0800 Subject: [PATCH 1/3] add clone command --- cli/clone/register.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cli/clone/register.ts diff --git a/cli/clone/register.ts b/cli/clone/register.ts new file mode 100644 index 0000000..e69de29 From 964063491fc5a7c8582b1bef497451f9e6f75261 Mon Sep 17 00:00:00 2001 From: seveibar Date: Sun, 5 Jan 2025 23:52:02 -0800 Subject: [PATCH 2/3] write register command --- README.md | 13 +++++++----- cli/clone/register.ts | 46 +++++++++++++++++++++++++++++++++++++++++++ cli/main.ts | 2 ++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c0d7014..0e1d3ef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# @tscircuit/snippets-cli +# tscircuit command line interface (CLI) -A CLI for developing, managing and publishing snippets on [tscircuit.com](https://tscircuit.com). +A CLI for developing, managing and publishing tscircuit code (the "npm for tscircuit") ## Usage @@ -8,7 +8,10 @@ A CLI for developing, managing and publishing snippets on [tscircuit.com](https: # Start a local server that watches for changes in # this file or it's dependencies and updates the # browser preview -snippets dev ./path/to/file.tsx +tsci dev ./path/to/file.tsx + +# Clone a snippet from the registry +tsci clone author/snippetName ``` > Note: The snippets CLI uses the same configuration files as the [@tscircuit/cli](https://github.com/tscircuit/cli), so you may need to also install `npm install -g @tscircuit/cli` and run `tsci login` to authenticate! @@ -16,7 +19,7 @@ snippets dev ./path/to/file.tsx ## Installation ```bash -npm install -g @tscircuit/snippets-cli +npm install -g @tscircuit/cli ``` ## Development @@ -47,5 +50,5 @@ runframe each time you'd like to load a new version of runframe. ```bash export RUNFRAME_STANDALONE_FILE_PATH=../runframe/dist/standalone.min.js cd ../runframe && bun run build -cd ../snippets-cli && bun run dev +cd ../cli && bun run dev ``` diff --git a/cli/clone/register.ts b/cli/clone/register.ts index e69de29..4fbb8e2 100644 --- a/cli/clone/register.ts +++ b/cli/clone/register.ts @@ -0,0 +1,46 @@ +import type { Command } from "commander" +import { getKy } from "lib/registry-api/get-ky" +import * as fs from "node:fs" +import * as path from "node:path" + +export const registerClone = (program: Command) => { + program + .command("clone") + .description("Clone a snippet from the registry") + .argument("", "Snippet to clone (format: author/snippetName)") + .action(async (snippetPath: string) => { + const [author, snippetName] = snippetPath.split("/") + + if (!author || !snippetName) { + console.error("Invalid snippet path. Use format: author/snippetName") + process.exit(1) + } + + const ky = getKy() + + try { + console.log(`Cloning ${author}/${snippetName}...`) + + const response = await ky + .get(`snippets/${author}/${snippetName}`) + .json() + + // Create directory if it doesn't exist + const dirPath = `./${snippetName}` + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath) + } + + // Write the snippet file + fs.writeFileSync( + path.join(dirPath, "snippet.tsx"), + response.snippet.content, + ) + + console.log(`Successfully cloned to ./${snippetName}/`) + } catch (error) { + console.error("Failed to clone snippet:", error.message) + process.exit(1) + } + }) +} diff --git a/cli/main.ts b/cli/main.ts index 8e2e7f9..0f31cf2 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -6,6 +6,7 @@ import { registerAuthLogout } from "./auth/logout/register" import { registerAuth } from "./auth/register" import { registerConfig } from "./config/register" import { registerConfigPrint } from "./config/print/register" +import { registerClone } from "./clone/register" import { perfectCli } from "perfect-cli" const program = new Command() @@ -16,6 +17,7 @@ program .version("1.0.0") registerDev(program) +registerClone(program) registerAuth(program) registerAuthLogin(program) From 7e8daa3ace71065a31ee0195c1faf6205fc92f3f Mon Sep 17 00:00:00 2001 From: seveibar Date: Mon, 6 Jan 2025 00:35:57 -0800 Subject: [PATCH 3/3] fix clone register command --- cli/clone/register.ts | 79 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/cli/clone/register.ts b/cli/clone/register.ts index 4fbb8e2..f4832b4 100644 --- a/cli/clone/register.ts +++ b/cli/clone/register.ts @@ -7,12 +7,23 @@ export const registerClone = (program: Command) => { program .command("clone") .description("Clone a snippet from the registry") - .argument("", "Snippet to clone (format: author/snippetName)") + .argument("", "Snippet to clone (e.g. author/snippetName)") .action(async (snippetPath: string) => { - const [author, snippetName] = snippetPath.split("/") + let author: string + let snippetName: string + if (!snippetPath.startsWith("@tsci/") && snippetPath.includes("/")) { + ;[author, snippetName] = snippetPath.split("/") + } else { + const trimmedPath = snippetPath.replace("@tsci/", "") + const firstDotIndex = trimmedPath.indexOf(".") + author = trimmedPath.slice(0, firstDotIndex) + snippetName = trimmedPath.slice(firstDotIndex + 1) + } if (!author || !snippetName) { - console.error("Invalid snippet path. Use format: author/snippetName") + console.error( + "Invalid snippet path. Use format: author/snippetName, author.snippetName or @tsci/author.snippetName", + ) process.exit(1) } @@ -21,25 +32,67 @@ export const registerClone = (program: Command) => { try { console.log(`Cloning ${author}/${snippetName}...`) - const response = await ky - .get(`snippets/${author}/${snippetName}`) + const packageFileList = await ky + .post<{ + package_files: Array<{ + package_file_id: string + package_release_id: string + file_path: string + created_at: string + }> + }>("package_files/list", { + json: { + package_name: `${author}/${snippetName}`, + use_latest_version: true, + }, + }) .json() // Create directory if it doesn't exist - const dirPath = `./${snippetName}` + const dirPath = `./${author}.${snippetName}` if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath) } - // Write the snippet file - fs.writeFileSync( - path.join(dirPath, "snippet.tsx"), - response.snippet.content, - ) + // Download each file that doesn't start with dist/ + for (const fileInfo of packageFileList.package_files) { + const filePath = fileInfo.file_path.startsWith("/") + ? fileInfo.file_path.slice(1) + : fileInfo.file_path + + if (filePath.startsWith("dist/")) continue + + const fileContent = await ky + .post<{ + package_file: { + content_text: string + } + }>("package_files/get", { + json: { + package_name: `${author}/${snippetName}`, + file_path: fileInfo.file_path, + }, + }) + .json() - console.log(`Successfully cloned to ./${snippetName}/`) + const fullPath = path.join(dirPath, filePath) + const dirName = path.dirname(fullPath) + + // Create nested directories if they don't exist + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }) + } + + fs.writeFileSync(fullPath, fileContent.package_file.content_text) + } + + console.log(`Successfully cloned to ./${author}.${snippetName}/`) } catch (error) { - console.error("Failed to clone snippet:", error.message) + if (error instanceof Error) { + console.error("Failed to clone snippet:", error.message) + } else { + console.error("Failed to clone snippet:", error) + } process.exit(1) } })