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 () => {
|
.command("install", "Install plugins from quartz.lock.json", CommonArgv, async () => {
|
||||||
await handleGitPluginInstall()
|
await handleGitPluginInstall()
|
||||||
})
|
})
|
||||||
.command("add <repos..>", "Add plugins from Git repositories", CommonArgv, async (argv) => {
|
.command(
|
||||||
await handlePluginAdd(argv.repos)
|
"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) => {
|
.command("remove <names..>", "Remove installed plugins", CommonArgv, async (argv) => {
|
||||||
await handlePluginRemove(argv.names)
|
await handlePluginRemove(argv.names)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -400,6 +400,76 @@ export function createConfigFromTemplate(templateName) {
|
|||||||
return rest
|
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 PLUGINS_JSON_PATH = CONFIG_YAML_PATH
|
||||||
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
|
export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH
|
||||||
export { LOCKFILE_PATH, PLUGINS_DIR }
|
export { LOCKFILE_PATH, PLUGINS_DIR }
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
isLocalSource,
|
isLocalSource,
|
||||||
getSourceUrl,
|
getSourceUrl,
|
||||||
formatSource,
|
formatSource,
|
||||||
|
resolveLockfileName,
|
||||||
|
getNameOverrides,
|
||||||
} from "./plugin-data.js"
|
} from "./plugin-data.js"
|
||||||
|
|
||||||
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
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()
|
let lockfile = readLockfile()
|
||||||
if (!lockfile) {
|
if (!lockfile) {
|
||||||
lockfile = { version: "1.0.0", plugins: {} }
|
lockfile = { version: "1.0.0", plugins: {} }
|
||||||
@@ -433,9 +447,21 @@ export async function handlePluginAdd(sources) {
|
|||||||
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
try {
|
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)
|
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)) {
|
if (fs.existsSync(pluginDir)) {
|
||||||
console.log(styleText("yellow", `⚠ ${name} already exists. Use 'update' to refresh.`))
|
console.log(styleText("yellow", `⚠ ${name} already exists. Use 'update' to refresh.`))
|
||||||
continue
|
continue
|
||||||
@@ -458,7 +484,7 @@ export async function handlePluginAdd(sources) {
|
|||||||
...(subdir && { subdir }),
|
...(subdir && { subdir }),
|
||||||
installedAt: new Date().toISOString(),
|
installedAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
addedPlugins.push({ name, pluginDir, source })
|
addedPlugins.push({ name, pluginDir, source, configSource })
|
||||||
console.log(styleText("green", `✓ Added ${name} (local symlink)`))
|
console.log(styleText("green", `✓ Added ${name} (local symlink)`))
|
||||||
} else if (subdir) {
|
} else if (subdir) {
|
||||||
console.log(styleText("cyan", `→ Adding ${name} from ${url} (subdir: ${subdir})...`))
|
console.log(styleText("cyan", `→ Adding ${name} from ${url} (subdir: ${subdir})...`))
|
||||||
@@ -472,7 +498,7 @@ export async function handlePluginAdd(sources) {
|
|||||||
subdir,
|
subdir,
|
||||||
installedAt: new Date().toISOString(),
|
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})`))
|
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)} (subdir: ${subdir})`))
|
||||||
} else {
|
} else {
|
||||||
console.log(styleText("cyan", `→ Adding ${name} from ${url}...`))
|
console.log(styleText("cyan", `→ Adding ${name} from ${url}...`))
|
||||||
@@ -494,7 +520,7 @@ export async function handlePluginAdd(sources) {
|
|||||||
installedAt: new Date().toISOString(),
|
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)}`))
|
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -517,10 +543,10 @@ export async function handlePluginAdd(sources) {
|
|||||||
writeLockfile(lockfile)
|
writeLockfile(lockfile)
|
||||||
const pluginsJson = readPluginsJson()
|
const pluginsJson = readPluginsJson()
|
||||||
if (pluginsJson?.plugins) {
|
if (pluginsJson?.plugins) {
|
||||||
for (const { pluginDir, source } of addedPlugins) {
|
for (const { pluginDir, source, configSource } of addedPlugins) {
|
||||||
const manifest = readManifestFromPackageJson(pluginDir)
|
const manifest = readManifestFromPackageJson(pluginDir)
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
source,
|
source: configSource ?? source,
|
||||||
enabled: manifest?.defaultEnabled ?? true,
|
enabled: manifest?.defaultEnabled ?? true,
|
||||||
options: manifest?.defaultOptions ?? {},
|
options: manifest?.defaultOptions ?? {},
|
||||||
order: manifest?.defaultOrder ?? 50,
|
order: manifest?.defaultOrder ?? 50,
|
||||||
@@ -553,23 +579,28 @@ export async function handlePluginRemove(names) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
let removed = false
|
let removed = false
|
||||||
|
const resolvedNames = []
|
||||||
for (const name of names) {
|
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`))
|
console.log(styleText("yellow", `⚠ ${name} is not installed`))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(styleText("cyan", `→ Removing ${name}...`))
|
const displayName = lockKey !== name ? `${name} (${lockKey})` : name
|
||||||
|
console.log(styleText("cyan", `→ Removing ${displayName}...`))
|
||||||
|
|
||||||
if (fs.existsSync(pluginDir)) {
|
if (fs.existsSync(pluginDir)) {
|
||||||
fs.rmSync(pluginDir, { recursive: true })
|
fs.rmSync(pluginDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
delete lockfile.plugins[name]
|
delete lockfile.plugins[lockKey]
|
||||||
console.log(styleText("green", `✓ Removed ${name}`))
|
console.log(styleText("green", `✓ Removed ${displayName}`))
|
||||||
removed = true
|
removed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,12 +609,12 @@ export async function handlePluginRemove(names) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeLockfile(lockfile)
|
writeLockfile(lockfile)
|
||||||
const pluginsJson = readPluginsJson()
|
|
||||||
if (pluginsJson?.plugins) {
|
if (pluginsJson?.plugins) {
|
||||||
pluginsJson.plugins = pluginsJson.plugins.filter(
|
pluginsJson.plugins = pluginsJson.plugins.filter(
|
||||||
(plugin) =>
|
(plugin) =>
|
||||||
!names.includes(extractPluginName(plugin.source)) &&
|
!names.includes(extractPluginName(plugin.source)) &&
|
||||||
!names.includes(formatSource(plugin.source)),
|
!names.includes(formatSource(plugin.source)) &&
|
||||||
|
!resolvedNames.includes(extractPluginName(plugin.source)),
|
||||||
)
|
)
|
||||||
writePluginsJson(pluginsJson)
|
writePluginsJson(pluginsJson)
|
||||||
}
|
}
|
||||||
@@ -704,14 +735,18 @@ export async function handlePluginCheck() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
const nameOverrides = getNameOverrides(lockfile, pluginsJson)
|
||||||
|
|
||||||
console.log(styleText("bold", "Checking for plugin updates...\n"))
|
console.log(styleText("bold", "Checking for plugin updates...\n"))
|
||||||
|
|
||||||
const results = []
|
const results = []
|
||||||
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
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") {
|
if (entry.commit === "local") {
|
||||||
results.push({
|
results.push({
|
||||||
name,
|
name: displayName,
|
||||||
installed: "local",
|
installed: "local",
|
||||||
latest: "—",
|
latest: "—",
|
||||||
status: "local",
|
status: "local",
|
||||||
@@ -729,14 +764,14 @@ export async function handlePluginCheck() {
|
|||||||
|
|
||||||
const isCurrent = latestCommit === entry.commit
|
const isCurrent = latestCommit === entry.commit
|
||||||
results.push({
|
results.push({
|
||||||
name,
|
name: displayName,
|
||||||
installed: entry.commit.slice(0, 7),
|
installed: entry.commit.slice(0, 7),
|
||||||
latest: latestCommit.slice(0, 7),
|
latest: latestCommit.slice(0, 7),
|
||||||
status: isCurrent ? "up to date" : "update available",
|
status: isCurrent ? "up to date" : "update available",
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
results.push({
|
results.push({
|
||||||
name,
|
name: displayName,
|
||||||
installed: entry.commit.slice(0, 7),
|
installed: entry.commit.slice(0, 7),
|
||||||
latest: "?",
|
latest: "?",
|
||||||
status: "check failed",
|
status: "check failed",
|
||||||
@@ -772,7 +807,10 @@ export async function handlePluginUpdate(names) {
|
|||||||
return
|
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 = []
|
const updatedPlugins = []
|
||||||
|
|
||||||
for (const name of pluginsToUpdate) {
|
for (const name of pluginsToUpdate) {
|
||||||
@@ -870,18 +908,24 @@ export async function handlePluginList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
const nameOverrides = getNameOverrides(lockfile, pluginsJson)
|
||||||
|
|
||||||
console.log(styleText("bold", "Installed Plugins:"))
|
console.log(styleText("bold", "Installed Plugins:"))
|
||||||
console.log()
|
console.log()
|
||||||
|
|
||||||
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
const pluginDir = path.join(PLUGINS_DIR, name)
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
const exists = fs.existsSync(pluginDir)
|
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") {
|
if (entry.commit === "local") {
|
||||||
const isLinked = exists && fs.lstatSync(pluginDir).isSymbolicLink()
|
const isLinked = exists && fs.lstatSync(pluginDir).isSymbolicLink()
|
||||||
const status = isLinked ? styleText("green", "✓") : styleText("red", "✗")
|
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(` Source: ${formatSource(entry.source)}`)
|
||||||
console.log(` Type: local symlink`)
|
console.log(` Type: local symlink`)
|
||||||
console.log(` Target: ${entry.resolved}`)
|
console.log(` Target: ${entry.resolved}`)
|
||||||
@@ -902,7 +946,7 @@ export async function handlePluginList() {
|
|||||||
: styleText("yellow", "⚡")
|
: styleText("yellow", "⚡")
|
||||||
: styleText("red", "✗")
|
: styleText("red", "✗")
|
||||||
|
|
||||||
console.log(` ${status} ${styleText("bold", name)}`)
|
console.log(` ${status} ${styleText("bold", displayLabel)}`)
|
||||||
console.log(` Source: ${formatSource(entry.source)}`)
|
console.log(` Source: ${formatSource(entry.source)}`)
|
||||||
console.log(` Commit: ${entry.commit.slice(0, 7)}`)
|
console.log(` Commit: ${entry.commit.slice(0, 7)}`)
|
||||||
if (currentCommit !== entry.commit && exists) {
|
if (currentCommit !== entry.commit && exists) {
|
||||||
|
|||||||
Reference in New Issue
Block a user