feat(cli): added flags for plugin name and subdir
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user