mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
Add comprehensive Git-based plugin CLI with lockfile support
- Create quartz.lock.json format for tracking exact plugin commits - Add 'npx quartz plugin' commands: install, add, remove, update, list, restore - Plugin state is fully reproducible via lockfile - No npm dependencies required for community plugins
This commit is contained in:
parent
5a6a2515ca
commit
574b7fa3a5
23
quartz.lock.json
Normal file
23
quartz.lock.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"plugins": {
|
||||||
|
"explorer": {
|
||||||
|
"source": "github:quartz-community/explorer",
|
||||||
|
"resolved": "https://github.com/quartz-community/explorer.git",
|
||||||
|
"commit": "6c97847cf1d200da2764554268c8f3d74a1edb6d",
|
||||||
|
"installedAt": "2026-02-08T12:00:00.000Z"
|
||||||
|
},
|
||||||
|
"graph": {
|
||||||
|
"source": "github:quartz-community/graph",
|
||||||
|
"resolved": "https://github.com/quartz-community/graph.git",
|
||||||
|
"commit": "c6a8cbb7e268999b131c1946ee4e97e1599157b4",
|
||||||
|
"installedAt": "2026-02-08T12:00:00.000Z"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"source": "github:quartz-community/search",
|
||||||
|
"resolved": "https://github.com/quartz-community/search.git",
|
||||||
|
"commit": "0131923f00c485664a50d1209a3e8eabde591856",
|
||||||
|
"installedAt": "2026-02-08T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,11 +9,13 @@ import {
|
|||||||
handleSync,
|
handleSync,
|
||||||
} from "./cli/handlers.js"
|
} from "./cli/handlers.js"
|
||||||
import {
|
import {
|
||||||
handlePluginInstall,
|
handlePluginInstall as handleGitPluginInstall,
|
||||||
|
handlePluginAdd,
|
||||||
|
handlePluginRemove,
|
||||||
|
handlePluginUpdate,
|
||||||
|
handlePluginRestore,
|
||||||
handlePluginList,
|
handlePluginList,
|
||||||
handlePluginSearch,
|
} from "./cli/plugin-git-handlers.js"
|
||||||
handlePluginUninstall,
|
|
||||||
} from "./cli/plugin-handlers.js"
|
|
||||||
import {
|
import {
|
||||||
CommonArgv,
|
CommonArgv,
|
||||||
BuildArgv,
|
BuildArgv,
|
||||||
@ -54,31 +56,32 @@ yargs(hideBin(process.argv))
|
|||||||
"Manage Quartz plugins",
|
"Manage Quartz plugins",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
|
.command("install", "Install plugins from quartz.lock.json", CommonArgv, async () => {
|
||||||
|
await handleGitPluginInstall()
|
||||||
|
})
|
||||||
|
.command("add <repos..>", "Add plugins from Git repositories", CommonArgv, async (argv) => {
|
||||||
|
await handlePluginAdd(argv.repos)
|
||||||
|
})
|
||||||
|
.command("remove <names..>", "Remove installed plugins", CommonArgv, async (argv) => {
|
||||||
|
await handlePluginRemove(argv.names)
|
||||||
|
})
|
||||||
.command(
|
.command(
|
||||||
"install <packages..>",
|
"update [names..]",
|
||||||
"Install Quartz plugins from npm",
|
"Update installed plugins to latest version",
|
||||||
PluginInstallArgv,
|
CommonArgv,
|
||||||
async (argv) => {
|
async (argv) => {
|
||||||
await handlePluginInstall(argv.packages)
|
await handlePluginUpdate(argv.names)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.command(
|
.command("list", "List all installed plugins", CommonArgv, async () => {
|
||||||
"uninstall <packages..>",
|
|
||||||
"Uninstall Quartz plugins",
|
|
||||||
PluginUninstallArgv,
|
|
||||||
async (argv) => {
|
|
||||||
await handlePluginUninstall(argv.packages)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.command("list", "List installed Quartz plugins", CommonArgv, async () => {
|
|
||||||
await handlePluginList()
|
await handlePluginList()
|
||||||
})
|
})
|
||||||
.command(
|
.command(
|
||||||
"search [query]",
|
"restore",
|
||||||
"Search for Quartz plugins on npm",
|
"Restore plugins from lockfile (exact versions)",
|
||||||
PluginSearchArgv,
|
CommonArgv,
|
||||||
async (argv) => {
|
async () => {
|
||||||
await handlePluginSearch(argv.query)
|
await handlePluginRestore()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.demandCommand(1, "Please specify a plugin subcommand")
|
.demandCommand(1, "Please specify a plugin subcommand")
|
||||||
|
|||||||
315
quartz/cli/plugin-git-handlers.js
Normal file
315
quartz/cli/plugin-git-handlers.js
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
import { styleText } from "util"
|
||||||
|
|
||||||
|
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
|
||||||
|
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
|
||||||
|
|
||||||
|
function readLockfile() {
|
||||||
|
if (!fs.existsSync(LOCKFILE_PATH)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(LOCKFILE_PATH, "utf-8")
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLockfile(lockfile) {
|
||||||
|
fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGitSource(source) {
|
||||||
|
if (source.startsWith("github:")) {
|
||||||
|
const [repoPath, ref] = source.replace("github:", "").split("#")
|
||||||
|
const [owner, repo] = repoPath.split("/")
|
||||||
|
return { name: repo, url: `https://github.com/${owner}/${repo}.git`, ref }
|
||||||
|
}
|
||||||
|
if (source.startsWith("git+")) {
|
||||||
|
const url = source.replace("git+", "")
|
||||||
|
const name = path.basename(url, ".git")
|
||||||
|
return { name, url }
|
||||||
|
}
|
||||||
|
if (source.startsWith("https://")) {
|
||||||
|
const name = path.basename(source, ".git")
|
||||||
|
return { name, url: source }
|
||||||
|
}
|
||||||
|
throw new Error(`Cannot parse plugin source: ${source}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGitCommit(pluginDir) {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim()
|
||||||
|
} catch {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginInstall() {
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
|
||||||
|
if (!lockfile) {
|
||||||
|
console.log(styleText("yellow", "⚠ No quartz.lock.json found. Run 'npx quartz plugin add <repo>' first."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("cyan", "→ Installing plugins from lockfile..."))
|
||||||
|
let installed = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
try {
|
||||||
|
const currentCommit = getGitCommit(pluginDir)
|
||||||
|
if (currentCommit === entry.commit) {
|
||||||
|
console.log(styleText("gray", ` ✓ ${name}@${entry.commit.slice(0, 7)} already installed`))
|
||||||
|
installed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
console.log(styleText("cyan", ` → ${name}: updating to ${entry.commit.slice(0, 7)}...`))
|
||||||
|
execSync("git fetch --depth 1 origin", { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
execSync(`git reset --hard ${entry.commit}`, { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
installed++
|
||||||
|
} catch {
|
||||||
|
console.log(styleText("red", ` ✗ ${name}: failed to update`))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
console.log(styleText("cyan", ` → ${name}: cloning...`))
|
||||||
|
execSync(`git clone --depth 1 ${entry.resolved} ${pluginDir}`, { stdio: "ignore" })
|
||||||
|
if (entry.commit !== "unknown") {
|
||||||
|
execSync(`git fetch --depth 1 origin ${entry.commit}`, { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
execSync(`git checkout ${entry.commit}`, { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
}
|
||||||
|
console.log(styleText("green", ` ✓ ${name}@${entry.commit.slice(0, 7)}`))
|
||||||
|
installed++
|
||||||
|
} catch {
|
||||||
|
console.log(styleText("red", ` ✗ ${name}: failed to clone`))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
if (failed === 0) {
|
||||||
|
console.log(styleText("green", `✓ Installed ${installed} plugin(s)`))
|
||||||
|
} else {
|
||||||
|
console.log(styleText("yellow", `⚠ Installed ${installed} plugin(s), ${failed} failed`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginAdd(sources) {
|
||||||
|
let lockfile = readLockfile()
|
||||||
|
if (!lockfile) {
|
||||||
|
lockfile = { version: "1.0.0", plugins: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
try {
|
||||||
|
const { name, url, ref } = parseGitSource(source)
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
console.log(styleText("yellow", `⚠ ${name} already exists. Use 'update' to refresh.`))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("cyan", `→ Adding ${name} from ${url}...`))
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
execSync(`git clone --depth 1 --branch ${ref} ${url} ${pluginDir}`, { stdio: "ignore" })
|
||||||
|
} else {
|
||||||
|
execSync(`git clone --depth 1 ${url} ${pluginDir}`, { stdio: "ignore" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const commit = getGitCommit(pluginDir)
|
||||||
|
lockfile.plugins[name] = {
|
||||||
|
source,
|
||||||
|
resolved: url,
|
||||||
|
commit,
|
||||||
|
installedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(styleText("red", `✗ Failed to add ${source}: ${error}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
console.log()
|
||||||
|
console.log(styleText("gray", "Updated quartz.lock.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginRemove(names) {
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
if (!lockfile) {
|
||||||
|
console.log(styleText("yellow", "⚠ No plugins installed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (!lockfile.plugins[name] && !fs.existsSync(pluginDir)) {
|
||||||
|
console.log(styleText("yellow", `⚠ ${name} is not installed`))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("cyan", `→ Removing ${name}...`))
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
fs.rmSync(pluginDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
delete lockfile.plugins[name]
|
||||||
|
console.log(styleText("green", `✓ Removed ${name}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
console.log()
|
||||||
|
console.log(styleText("gray", "Updated quartz.lock.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginUpdate(names) {
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
if (!lockfile) {
|
||||||
|
console.log(styleText("yellow", "⚠ No plugins installed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginsToUpdate = names || Object.keys(lockfile.plugins)
|
||||||
|
|
||||||
|
for (const name of pluginsToUpdate) {
|
||||||
|
const entry = lockfile.plugins[name]
|
||||||
|
if (!entry) {
|
||||||
|
console.log(styleText("yellow", `⚠ ${name} is not installed`))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
if (!fs.existsSync(pluginDir)) {
|
||||||
|
console.log(styleText("yellow", `⚠ ${name} directory missing. Run 'npx quartz plugin install'.`))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(styleText("cyan", `→ Updating ${name}...`))
|
||||||
|
execSync("git fetch --depth 1 origin", { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
execSync("git reset --hard origin/HEAD", { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
|
||||||
|
const newCommit = getGitCommit(pluginDir)
|
||||||
|
if (newCommit !== entry.commit) {
|
||||||
|
entry.commit = newCommit
|
||||||
|
entry.installedAt = new Date().toISOString()
|
||||||
|
console.log(styleText("green", `✓ Updated ${name} to ${newCommit.slice(0, 7)}`))
|
||||||
|
} else {
|
||||||
|
console.log(styleText("gray", `✓ ${name} already up to date`))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(styleText("red", `✗ Failed to update ${name}: ${error}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
console.log()
|
||||||
|
console.log(styleText("gray", "Updated quartz.lock.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginList() {
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
|
||||||
|
console.log(styleText("gray", "No plugins installed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("bold", "Installed Plugins:"))
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
const exists = fs.existsSync(pluginDir)
|
||||||
|
let currentCommit = entry.commit
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
currentCommit = getGitCommit(pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = exists
|
||||||
|
? currentCommit === entry.commit
|
||||||
|
? styleText("green", "✓")
|
||||||
|
: styleText("yellow", "⚡")
|
||||||
|
: styleText("red", "✗")
|
||||||
|
|
||||||
|
console.log(` ${status} ${styleText("bold", name)}`)
|
||||||
|
console.log(` Source: ${entry.source}`)
|
||||||
|
console.log(` Commit: ${entry.commit.slice(0, 7)}`)
|
||||||
|
if (currentCommit !== entry.commit && exists) {
|
||||||
|
console.log(` Current: ${currentCommit.slice(0, 7)} (modified)`)
|
||||||
|
}
|
||||||
|
console.log(` Installed: ${new Date(entry.installedAt).toLocaleDateString()}`)
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginRestore() {
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
if (!lockfile) {
|
||||||
|
console.log(styleText("red", "✗ No quartz.lock.json found. Cannot restore."))
|
||||||
|
console.log()
|
||||||
|
console.log("Run 'npx quartz plugin add <repo>' to install plugins from scratch.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("cyan", "→ Restoring plugins from lockfile..."))
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
const pluginsDir = path.join(process.cwd(), ".quartz", "plugins")
|
||||||
|
if (!fs.existsSync(pluginsDir)) {
|
||||||
|
fs.mkdirSync(pluginsDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
|
const pluginDir = path.join(pluginsDir, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
console.log(styleText("yellow", `⚠ ${name}: directory exists, skipping`))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(styleText("cyan", `→ ${name}: cloning ${entry.resolved}@${entry.commit.slice(0, 7)}...`))
|
||||||
|
execSync(`git clone ${entry.resolved} ${pluginDir}`, { stdio: "ignore" })
|
||||||
|
execSync(`git checkout ${entry.commit}`, { cwd: pluginDir, stdio: "ignore" })
|
||||||
|
console.log(styleText("green", `✓ ${name} restored`))
|
||||||
|
installed++
|
||||||
|
} catch {
|
||||||
|
console.log(styleText("red", `✗ ${name}: failed to restore`))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
if (failed === 0) {
|
||||||
|
console.log(styleText("green", `✓ Restored ${installed} plugin(s)`))
|
||||||
|
} else {
|
||||||
|
console.log(styleText("yellow", `⚠ Restored ${installed} plugin(s), ${failed} failed`))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user