akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
250 lines (249 loc) • 11.2 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Setup wizard steps for stash sources and registries: toggle configured
* sources, pull registry-recommended stashes, add custom sources, and toggle
* built-in registries.
*/
import * as p from "../../cli/clack.js";
import { DEFAULT_CONFIG, getEffectiveRegistries, getSources } from "../../core/config/config.js";
import { readLockfile } from "../../integrations/lockfile.js";
import { prompt, promptOrBack } from "../prompt.js";
import { loadSetupStashes } from "../registry-stash-loader.js";
import { canonicalSetupGitUrl } from "../source-identity.js";
function configuredSourceKey(source) {
return `${source.type}:${source.path ?? source.url ?? source.name ?? "unknown"}`;
}
function configuredSourceTarget(source) {
return source.type === "npm" ? source.path : source.url;
}
function configuredSourceIdentity(source) {
const target = configuredSourceTarget(source);
return source.type === "git" && target ? canonicalSetupGitUrl(target) : target;
}
function recommendedSourceIdentity(entry) {
return entry.installType === "git" ? canonicalSetupGitUrl(entry.url) : entry.url;
}
function describeConfiguredSource(source) {
const target = source.path ?? source.url ?? "(unknown target)";
const typeLabel = source.type === "git" ? "Git" : source.type === "filesystem" ? "Filesystem" : source.type;
return {
value: configuredSourceKey(source),
label: source.name ?? target,
hint: `${typeLabel}: ${target}`,
};
}
function renderConfiguredSourceList(sources) {
return sources
.map((source) => {
const described = describeConfiguredSource(source);
return `- ${described.label} (${described.hint})`;
})
.join("\n");
}
function renderInstalledSourceList(managed) {
return managed.map((entry) => `- ${entry.name ?? entry.url ?? entry.path ?? "unknown"} (${entry.type})`).join("\n");
}
export async function stepAdditionalSources(currentSources) {
const sources = [...currentSources];
let addMore = true;
while (addMore) {
const action = await prompt(() => p.select({
message: "Add another bundle source?",
options: [
{ value: "done", label: "Done — no more sources" },
{ value: "github-repo", label: "GitHub repository", hint: "custom URL" },
{ value: "filesystem", label: "Filesystem path", hint: "local directory" },
],
initialValue: "done",
}));
if (action === "done") {
addMore = false;
break;
}
if (action === "github-repo") {
const url = await promptOrBack(() => p.text({
message: "Enter the GitHub repository URL:",
placeholder: "https://github.com/owner/repo",
validate: (v) => {
if (!v?.trim())
return "URL cannot be empty";
},
}));
if (url === null)
continue;
const name = await promptOrBack(() => p.text({
message: "Give this bundle a name (optional):",
placeholder: "my-repo",
}));
if (name === null)
continue;
const entryUrl = url.trim();
const entry = { type: "git", url: entryUrl };
if (name.trim())
entry.name = name.trim();
if (!sources.some((source) => source.type === "git" &&
source.url !== undefined &&
canonicalSetupGitUrl(source.url) === canonicalSetupGitUrl(entryUrl))) {
sources.push(entry);
}
else {
p.log.warn("This URL is already configured.");
}
}
if (action === "filesystem") {
const fsPath = await promptOrBack(() => p.text({
message: "Enter the directory path:",
placeholder: "/path/to/bundle",
validate: (v) => {
if (!v?.trim())
return "Path cannot be empty";
},
}));
if (fsPath === null)
continue;
const resolved = fsPath.trim();
const name = await promptOrBack(() => p.text({
message: "Give this bundle a name (optional):",
placeholder: "my-bundle",
}));
if (name === null)
continue;
const entry = { type: "filesystem", path: resolved };
if (name.trim())
entry.name = name.trim();
if (!sources.some((s) => s.path === entry.path)) {
sources.push(entry);
}
else {
p.log.warn("This path is already configured.");
}
}
}
return sources;
}
export async function stepRegistries(current) {
const defaults = DEFAULT_CONFIG.registries ?? [];
const currentRegistries = current.registries ?? defaults;
const defaultUrls = new Set(defaults.map((r) => r.url));
const enabledUrls = new Set(currentRegistries.filter((r) => r.enabled !== false).map((r) => r.url));
// Collect custom (non-default) registries to preserve them
const customRegistries = currentRegistries.filter((r) => !defaultUrls.has(r.url));
// Show default registries for toggling
const options = defaults.map((r) => ({
value: r.url,
label: r.name ?? r.url,
hint: r.provider ?? "static index",
}));
if (customRegistries.length > 0) {
p.log.info(`You have ${customRegistries.length} custom registr${customRegistries.length === 1 ? "y" : "ies"} that will be preserved.`);
}
const selected = await prompt(() => p.multiselect({
message: "Which built-in registries should be enabled?",
options,
initialValues: options.filter((o) => enabledUrls.has(o.value)).map((o) => o.value),
}));
// If all defaults are selected and there are no custom registries,
// return undefined to use the built-in defaults (avoids pinning)
const allDefaultsSelected = defaults.every((r) => selected.includes(r.url));
if (allDefaultsSelected && customRegistries.length === 0) {
return undefined;
}
// Build explicit list: toggled defaults + preserved custom registries
const result = defaults.map((r) => ({
...r,
enabled: selected.includes(r.url),
}));
// Re-add custom registries unchanged
for (const custom of customRegistries) {
result.push(custom);
}
return result;
}
/**
* @internal Exported for testing only.
*/
export async function stepAddSources(current, options) {
// The wizard toggles plain additional bundles, not the default bundle or
// lock-backed registry installs, which are preserved untouched.
const lockIds = new Set(readLockfile().map((entry) => entry.id));
const allSources = getSources(current);
const existingSources = allSources.filter((source) => !source.primary && !(source.name && lockIds.has(source.name)));
const managedSources = allSources.filter((source) => source.name != null && lockIds.has(source.name));
const sources = [];
if (existingSources.length > 0) {
p.note(renderConfiguredSourceList(existingSources), "Configured bundle sources");
const options = existingSources.map(describeConfiguredSource);
const selected = await prompt(() => p.multiselect({
message: "Configured bundle sources — uncheck any you want to disable:",
options,
initialValues: options.map((option) => option.value),
required: false,
}));
for (const source of existingSources) {
if (selected.includes(configuredSourceKey(source))) {
sources.push(source);
}
}
}
if (managedSources.length > 0) {
p.note(renderInstalledSourceList(managedSources), "Installed managed bundles (preserved)");
}
// ── Registry-driven stash recommendations ─────────────────────────────
// Fetch available stashes from the official registry (cached, stale-ok).
// Falls back to the bundled list when the registry is unreachable.
const registryUrl = getEffectiveRegistries(current)[0]?.url ??
"https://raw.githubusercontent.com/itlackey/akm-registry/main/index.json";
const availableStashes = await loadSetupStashes(registryUrl);
if (availableStashes.length > 0) {
const existingTargets = new Set(sources.map(configuredSourceIdentity).filter((value) => !!value));
const stashOptions = availableStashes.map((s) => ({
value: recommendedSourceIdentity(s),
label: s.name,
hint: existingTargets.has(recommendedSourceIdentity(s))
? `${s.description} (already added)`
: s.description || s.source,
}));
// Pre-check: already-installed stashes OR default-selected on fresh install
const initialValues = sources.length > 0
? stashOptions.filter((o) => existingTargets.has(o.value)).map((o) => o.value)
: availableStashes.filter((s) => s.defaultSelected).map(recommendedSourceIdentity);
const selectedUrls = await prompt(() => p.multiselect({
message: availableStashes[0]?.source === "registry"
? "Available bundles from the AKM registry — toggle to add or remove:"
: "Recommended bundle sources — toggle to add or remove:",
options: stashOptions,
initialValues,
required: false,
}));
// Add newly selected stashes
for (const target of selectedUrls) {
if (!existingTargets.has(target)) {
const entry = availableStashes.find((s) => recommendedSourceIdentity(s) === target);
if (!entry)
continue;
sources.push(entry.installType === "npm"
? { type: "npm", path: target, name: entry.name }
: { type: "git", url: target, name: entry.name });
existingTargets.add(target);
}
}
// Remove deselected stashes that were previously configured
for (const entry of availableStashes) {
const target = recommendedSourceIdentity(entry);
if (existingTargets.has(target) && !selectedUrls.includes(target)) {
const idx = sources.findIndex((source) => configuredSourceIdentity(source) === target);
if (idx !== -1) {
sources.splice(idx, 1);
existingTargets.delete(target);
p.log.info(`Removed ${entry.name}.`);
}
}
}
}
if (options?.promptForAdditional === false) {
return sources;
}
return stepAdditionalSources(sources);
}