feat(cli): added flags for plugin name and subdir

This commit is contained in:
saberzero1
2026-03-25 22:05:31 +01:00
parent f881d5e332
commit 2d579c0f9e
3 changed files with 155 additions and 25 deletions

View File

@@ -122,9 +122,25 @@ yargs(hideBin(process.argv))
.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(
"add <repos..>",
"Add plugins from Git repositories",
{
...CommonArgv,
name: {
string: true,
alias: ["as"],
describe: "Override the plugin name (for resolving conflicts with duplicate names)",
},
subdir: {
string: true,
describe: "Subdirectory within the repository containing the plugin",
},
},
async (argv) => {
await handlePluginAdd(argv.repos, { name: argv.name, subdir: argv.subdir })
},
)
.command("remove <names..>", "Remove installed plugins", CommonArgv, async (argv) => {
await handlePluginRemove(argv.names)
})

View File

@@ -400,6 +400,76 @@ export function createConfigFromTemplate(templateName) {
return rest
}
/**
* Resolves a user-facing plugin name (which may be an overridden name from config)
* to the corresponding lockfile key (the original name at install time).
*
* This bridges the naming identity split between config YAML (which supports
* source.name overrides) and the lockfile/disk (which are keyed by the original name).
*
* @param {string} name - The name the user provided (may be overridden or original)
* @param {object|null} lockfile - The parsed lockfile
* @param {object|null} pluginsJson - The parsed config YAML
* @returns {string} The lockfile key that corresponds to this plugin
*/
export function resolveLockfileName(name, lockfile, pluginsJson) {
// Direct match — no resolution needed
if (lockfile?.plugins?.[name]) return name
// Check if any config entry with this overridden name maps to a different lockfile key
if (pluginsJson?.plugins) {
const configEntry = pluginsJson.plugins.find(
(e) => extractPluginName(e.source) === name || formatSource(e.source) === name,
)
if (configEntry) {
const url = getSourceUrl(configEntry.source)
for (const [key, lock] of Object.entries(lockfile?.plugins ?? {})) {
if (
lock.source === url ||
lock.source === formatSource(configEntry.source) ||
lock.resolved === url
) {
return key
}
}
}
}
return name
}
/**
* Builds a map from lockfile keys to their overridden display names from config.
* Returns entries only where the overridden name differs from the lockfile key.
*
* @param {object|null} lockfile - The parsed lockfile
* @param {object|null} pluginsJson - The parsed config YAML
* @returns {Map<string, string>} Map of lockfileKey → overriddenName
*/
export function getNameOverrides(lockfile, pluginsJson) {
const overrides = new Map()
if (!lockfile?.plugins || !pluginsJson?.plugins) return overrides
for (const entry of pluginsJson.plugins) {
const configName = extractPluginName(entry.source)
const url = getSourceUrl(entry.source)
for (const [lockKey, lock] of Object.entries(lockfile.plugins)) {
if (lockKey === configName) break // no override, names match
if (
lock.source === url ||
lock.source === formatSource(entry.source) ||
lock.resolved === url
) {
overrides.set(lockKey, configName)
break
}
}
}
return overrides
}
export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
export { LOCKFILE_PATH, PLUGINS_DIR }

View File

@@ -17,6 +17,8 @@ import {
isLocalSource,
getSourceUrl,
formatSource,
resolveLockfileName,
getNameOverrides,
} from "./plugin-data.js"
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
@@ -419,7 +421,19 @@ export async function handlePluginInstall() {
}
}
export async function handlePluginAdd(sources) {
export async function handlePluginAdd(
sources,
{ name: nameOverride, subdir: subdirOverride } = {},
) {
if (nameOverride && sources.length > 1) {
console.log(styleText("red", "✗ --name/--as can only be used when adding a single plugin"))
return
}
if (subdirOverride && sources.length > 1) {
console.log(styleText("red", "✗ --subdir can only be used when adding a single plugin"))
return
}
let lockfile = readLockfile()
if (!lockfile) {
lockfile = { version: "1.0.0", plugins: {} }
@@ -433,9 +447,21 @@ export async function handlePluginAdd(sources) {
for (const source of sources) {
try {
const { name, url, ref, local, subdir } = parseGitSource(source)
const parsed = parseGitSource(source)
const name = nameOverride ?? parsed.name
const url = parsed.url
const ref = parsed.ref
const local = parsed.local
const subdir = subdirOverride ?? parsed.subdir
const pluginDir = path.join(PLUGINS_DIR, name)
let configSource = undefined
if (nameOverride || subdirOverride) {
configSource = { repo: source }
if (nameOverride) configSource.name = nameOverride
if (subdirOverride) configSource.subdir = subdirOverride
}
if (fs.existsSync(pluginDir)) {
console.log(styleText("yellow", `${name} already exists. Use 'update' to refresh.`))
continue
@@ -458,7 +484,7 @@ export async function handlePluginAdd(sources) {
...(subdir && { subdir }),
installedAt: new Date().toISOString(),
}
addedPlugins.push({ name, pluginDir, source })
addedPlugins.push({ name, pluginDir, source, configSource })
console.log(styleText("green", `✓ Added ${name} (local symlink)`))
} else if (subdir) {
console.log(styleText("cyan", `→ Adding ${name} from ${url} (subdir: ${subdir})...`))
@@ -472,7 +498,7 @@ export async function handlePluginAdd(sources) {
subdir,
installedAt: new Date().toISOString(),
}
addedPlugins.push({ name, pluginDir, source })
addedPlugins.push({ name, pluginDir, source, configSource })
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`))
} else {
console.log(styleText("cyan", `→ Adding ${name} from ${url}...`))
@@ -494,7 +520,7 @@ export async function handlePluginAdd(sources) {
installedAt: new Date().toISOString(),
}
addedPlugins.push({ name, pluginDir, source })
addedPlugins.push({ name, pluginDir, source, configSource })
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`))
}
} catch (error) {
@@ -517,10 +543,10 @@ export async function handlePluginAdd(sources) {
writeLockfile(lockfile)
const pluginsJson = readPluginsJson()
if (pluginsJson?.plugins) {
for (const { pluginDir, source } of addedPlugins) {
for (const { pluginDir, source, configSource } of addedPlugins) {
const manifest = readManifestFromPackageJson(pluginDir)
const newEntry = {
source,
source: configSource ?? source,
enabled: manifest?.defaultEnabled ?? true,
options: manifest?.defaultOptions ?? {},
order: manifest?.defaultOrder ?? 50,
@@ -553,23 +579,28 @@ export async function handlePluginRemove(names) {
return
}
const pluginsJson = readPluginsJson()
let removed = false
const resolvedNames = []
for (const name of names) {
const pluginDir = path.join(PLUGINS_DIR, name)
const lockKey = resolveLockfileName(name, lockfile, pluginsJson)
resolvedNames.push(lockKey)
const pluginDir = path.join(PLUGINS_DIR, lockKey)
if (!lockfile.plugins[name] && !fs.existsSync(pluginDir)) {
if (!lockfile.plugins[lockKey] && !fs.existsSync(pluginDir)) {
console.log(styleText("yellow", `${name} is not installed`))
continue
}
console.log(styleText("cyan", `→ Removing ${name}...`))
const displayName = lockKey !== name ? `${name} (${lockKey})` : name
console.log(styleText("cyan", `→ Removing ${displayName}...`))
if (fs.existsSync(pluginDir)) {
fs.rmSync(pluginDir, { recursive: true })
}
delete lockfile.plugins[name]
console.log(styleText("green", `✓ Removed ${name}`))
delete lockfile.plugins[lockKey]
console.log(styleText("green", `✓ Removed ${displayName}`))
removed = true
}
@@ -578,12 +609,12 @@ export async function handlePluginRemove(names) {
}
writeLockfile(lockfile)
const pluginsJson = readPluginsJson()
if (pluginsJson?.plugins) {
pluginsJson.plugins = pluginsJson.plugins.filter(
(plugin) =>
!names.includes(extractPluginName(plugin.source)) &&
!names.includes(formatSource(plugin.source)),
!names.includes(formatSource(plugin.source)) &&
!resolvedNames.includes(extractPluginName(plugin.source)),
)
writePluginsJson(pluginsJson)
}
@@ -704,14 +735,18 @@ export async function handlePluginCheck() {
return
}
const pluginsJson = readPluginsJson()
const nameOverrides = getNameOverrides(lockfile, pluginsJson)
console.log(styleText("bold", "Checking for plugin updates...\n"))
const results = []
for (const [name, entry] of Object.entries(lockfile.plugins)) {
// Local plugins: show "local" status, skip git checks
const displayName = nameOverrides.get(name) ?? name
if (entry.commit === "local") {
results.push({
name,
name: displayName,
installed: "local",
latest: "—",
status: "local",
@@ -729,14 +764,14 @@ export async function handlePluginCheck() {
const isCurrent = latestCommit === entry.commit
results.push({
name,
name: displayName,
installed: entry.commit.slice(0, 7),
latest: latestCommit.slice(0, 7),
status: isCurrent ? "up to date" : "update available",
})
} catch {
results.push({
name,
name: displayName,
installed: entry.commit.slice(0, 7),
latest: "?",
status: "check failed",
@@ -772,7 +807,10 @@ export async function handlePluginUpdate(names) {
return
}
const pluginsToUpdate = names || Object.keys(lockfile.plugins)
const pluginsJson = readPluginsJson()
const pluginsToUpdate = names
? names.map((n) => resolveLockfileName(n, lockfile, pluginsJson))
: Object.keys(lockfile.plugins)
const updatedPlugins = []
for (const name of pluginsToUpdate) {
@@ -870,18 +908,24 @@ export async function handlePluginList() {
return
}
const pluginsJson = readPluginsJson()
const nameOverrides = getNameOverrides(lockfile, pluginsJson)
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)
const overriddenName = nameOverrides.get(name)
const displayLabel = overriddenName
? `${overriddenName} ${styleText("gray", `(dir: ${name})`)}`
: name
// Local plugins: special display
if (entry.commit === "local") {
const isLinked = exists && fs.lstatSync(pluginDir).isSymbolicLink()
const status = isLinked ? styleText("green", "✓") : styleText("red", "✗")
console.log(` ${status} ${styleText("bold", name)}`)
console.log(` ${status} ${styleText("bold", displayLabel)}`)
console.log(` Source: ${formatSource(entry.source)}`)
console.log(` Type: local symlink`)
console.log(` Target: ${entry.resolved}`)
@@ -902,7 +946,7 @@ export async function handlePluginList() {
: styleText("yellow", "⚡")
: styleText("red", "✗")
console.log(` ${status} ${styleText("bold", name)}`)
console.log(` ${status} ${styleText("bold", displayLabel)}`)
console.log(` Source: ${formatSource(entry.source)}`)
console.log(` Commit: ${entry.commit.slice(0, 7)}`)
if (currentCommit !== entry.commit && exists) {