UNPKG

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

685 lines (684 loc) • 31.4 kB
// 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/. /** * Plain-text renderers for the remaining leaf `akm` commands: info/config/ * feedback/remember/import/save/enable/disable, registry, env, wiki, * events/history, search/curate, and the stash-lifecycle verbs (init, * index, list, add, remove, update, upgrade, clone). * * Split out of `helpers.ts` (formerly 1418 lines / 59 fns) as its own * sibling module: unlike `show`/`workflow`/`proposal` (each a stateful, * multi-branch renderer with its own module), these are flat * field-extraction formatters with no shared state beyond * `formatRegistrySearchPlain` delegating to `formatSearchPlain` in this * same file. */ import { formatRegistryUrl } from "../../core/registry-url.js"; /** Render the current `InfoResponse` shape. */ export function formatInfoPlain(r) { const lines = []; if (r.version) lines.push(`version: ${String(r.version)}`); // Only present in infoCommand's own last-resort fallback (stash-cli.ts), // when assembleInfo() itself threw — every field below it belongs to the // normal shape and is absent there. if (typeof r.error === "string") lines.push(`error: ${r.error}`); if (r.bundleDir) lines.push(`bundleDir: ${String(r.bundleDir)}`); if (r.defaultBundle !== undefined) { lines.push(`defaultBundle: ${r.defaultBundle === null ? "(none)" : String(r.defaultBundle)}`); } if (typeof r.configError === "string") lines.push(`configError: ${r.configError}`); if (typeof r.bundleDirError === "string") lines.push(`bundleDirError: ${r.bundleDirError}`); if (Array.isArray(r.assetTypes) && r.assetTypes.length > 0) { lines.push(`assetTypes: ${r.assetTypes.join(", ")}`); } if (Array.isArray(r.searchModes) && r.searchModes.length > 0) { lines.push(`searchModes: ${r.searchModes.join(", ")}`); } const semanticSearch = r.semanticSearch; if (semanticSearch) { lines.push("semanticSearch:"); for (const [k, v] of Object.entries(semanticSearch)) { lines.push(` ${k}: ${String(v)}`); } } const registries = Array.isArray(r.registries) ? r.registries : undefined; if (registries) { lines.push(`registries (${registries.length}):`); for (const reg of registries) { const name = typeof reg.name === "string" ? `${reg.name}: ` : ""; const provider = typeof reg.provider === "string" ? ` (${reg.provider})` : ""; const disabled = reg.enabled === false ? " [disabled]" : ""; const url = typeof reg.url === "string" ? formatRegistryUrl(reg.url) : "?"; lines.push(` ${name}${url}${provider}${disabled}`); } } const sourceProviders = Array.isArray(r.sourceProviders) ? r.sourceProviders : undefined; if (sourceProviders) { lines.push(`sourceProviders (${sourceProviders.length}):`); for (const source of sourceProviders) { const label = source.name ?? source.path ?? source.url ?? source.type ?? "?"; const disabled = source.enabled === false ? " [disabled]" : ""; lines.push(` [${String(source.type ?? "?")}] ${String(label)}${disabled}`); } } const indexStats = r.indexStats; if (indexStats) { lines.push("indexStats:"); for (const [k, v] of Object.entries(indexStats)) { lines.push(` ${k}: ${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`); } } if (lines.length === 0) return JSON.stringify(r, null, 2); return lines.join("\n"); } export function formatConfigPlain(r) { // Recursive flattener: prints `key=value` lines, and nested objects as // `parent.child=value`. Arrays render as JSON for compactness. const lines = []; const walk = (obj, prefix) => { for (const [k, v] of Object.entries(obj)) { const path = prefix ? `${prefix}.${k}` : k; if (v === null || v === undefined) { lines.push(`${path}=`); } else if (Array.isArray(v)) { lines.push(`${path}=${JSON.stringify(v)}`); } else if (typeof v === "object") { walk(v, path); } else { const rendered = typeof v === "string" && /^registries\.\d+\.url$/u.test(path) ? formatRegistryUrl(v) : String(v); lines.push(`${path}=${rendered}`); } } }; walk(r, ""); if (lines.length === 0) return "(empty config)"; return lines.join("\n"); } export function formatFeedbackPlain(r) { const ref = String(r.ref ?? "?"); const signal = String(r.signal ?? "?"); const note = typeof r.note === "string" && r.note ? ` — ${r.note}` : ""; return `Recorded ${signal} feedback for ${ref}${note}`; } export function formatRememberPlain(r) { const ref = String(r.ref ?? "?"); const pathValue = String(r.path ?? "?"); return `Saved ${ref} at ${pathValue}`; } export function formatImportPlain(r) { const ref = String(r.ref ?? "?"); const source = String(r.source ?? "?"); const pathValue = String(r.path ?? "?"); return `Imported ${source} → ${ref} at ${pathValue}`; } export function formatSyncPlain(r) { if (r.ok === false) { const reason = typeof r.reason === "string" ? r.reason : "unknown"; return `sync: failed (${reason})`; } const name = typeof r.name === "string" ? r.name : "primary bundle"; const committed = r.committed === true; const pushed = r.pushed === true; const parts = [`sync: ${name}`]; parts.push(committed ? "committed" : "no changes"); if (pushed) parts.push("pushed"); return parts.join(" — "); } export function formatRegistryListPlain(r) { const registries = Array.isArray(r.registries) ? r.registries : []; if (registries.length === 0) { return "No registries configured. Add one with `akm registry add <url>`."; } const lines = []; for (const reg of registries) { const url = typeof reg.url === "string" ? formatRegistryUrl(reg.url) : "?"; const name = typeof reg.name === "string" ? reg.name : ""; const provider = typeof reg.provider === "string" ? ` (${reg.provider})` : ""; const enabled = reg.enabled === false ? " [disabled]" : ""; const head = name ? `${name}: ${url}` : url; lines.push(`${head}${provider}${enabled}`); } return lines.join("\n"); } export function formatRegistryAddPlain(r) { if (r.added === false) { return typeof r.message === "string" ? r.message : "Registry already configured."; } const registries = Array.isArray(r.registries) ? r.registries.length : 0; return `Registry added (${registries} total).`; } export function formatRegistryRemovePlain(r) { if (r.removed === false) { return typeof r.message === "string" ? r.message : "No matching registry found."; } const entry = r.entry; const url = entry ? (typeof entry.url === "string" ? formatRegistryUrl(entry.url) : String(entry.name ?? "?")) : "?"; return `Removed registry ${url}`; } export function formatRegistrySearchPlain(r, detail) { // Reuse the same renderer as `search` — both share `hits` / `registryHits`. return formatSearchPlain(r, detail); } export function formatEnvListPlain(r) { // Multi-env listing: { envs: [{ ref, path, keys }, ...] } const envs = Array.isArray(r.envs) ? r.envs : []; if (envs.length === 0) { return "No env files. Create one with `akm env create <name>`, then edit the .env file directly."; } const lines = []; for (const v of envs) { const ref = String(v.ref ?? "?"); const keys = Array.isArray(v.keys) ? v.keys.map(String) : []; if (lines.length > 0) lines.push(""); lines.push(`## ${ref}`); if (keys.length === 0) { lines.push("- (no keys)"); continue; } for (const key of keys) { lines.push(`- ${key}`); } } return lines.join("\n"); } export function formatEnvCreatePlain(r) { return `Created env ${String(r.ref ?? "?")}`; } export function formatEnvExportPlain(r) { return `Wrote ${String(r.ref ?? "?")} export script → ${String(r.out ?? "?")} (mode 0600; source it, then delete)`; } export function formatEnvRemovePlain(r) { const removed = r.removed === true; return removed ? `Removed env ${String(r.ref ?? "?")}` : `Env ${String(r.ref ?? "?")} was not present`; } export function formatEventsPlain(r) { const events = Array.isArray(r.events) ? r.events : []; const headerParts = []; if (typeof r.ref === "string" && r.ref) headerParts.push(`ref: ${r.ref}`); if (typeof r.type === "string" && r.type) headerParts.push(`type: ${r.type}`); if (typeof r.since === "string" && r.since) headerParts.push(`since: ${r.since}`); const totalCount = typeof r.totalCount === "number" ? r.totalCount : events.length; headerParts.push(`${totalCount} event(s)`); const header = headerParts.join(" "); if (events.length === 0) { return `${header}\nNo events.`; } const lines = [header, ""]; for (const event of events) { lines.push(formatEventLine(event)); } return lines.join("\n").trimEnd(); } export function formatEventLine(event) { const ts = String(event.ts ?? "?"); const eventType = String(event.eventType ?? "?"); const ref = event.ref ? String(event.ref) : null; const head = ref ? `${ts} [${eventType}] ${ref}` : `${ts} [${eventType}]`; if (event.metadata != null && event.metadata !== "") { const meta = typeof event.metadata === "string" ? event.metadata : JSON.stringify(event.metadata); return `${head}\n metadata: ${meta}`; } return head; } export function formatSearchPlain(r, detail) { const hits = r.hits ?? []; const registryHits = r.registryHits ?? []; const allHits = [...hits, ...registryHits]; if (allHits.length === 0) { const warnings = Array.isArray(r.warnings) ? r.warnings : []; const hasSetupWarning = warnings.some((w) => String(w).toLowerCase().includes("no bundle") || String(w).toLowerCase().includes("not configured")); if (hasSetupWarning) { return "No bundle configured. Run `akm bundle create` to create your working bundle, then `akm index` to build the search index."; } const base = r.tip ? String(r.tip) : "No matches found."; return `${base}\nTry:\n akm search '<broader-term>' # fewer keywords\n akm bundle list # see all configured sources\n akm curate '<query>' # let akm select the best match`; } const lines = []; for (const hit of allHits) { const type = hit.type ?? "unknown"; const name = hit.name ?? "unnamed"; const score = hit.score != null ? ` (score: ${hit.score})` : ""; const desc = hit.description ? ` ${hit.description}` : ""; lines.push(`${type}: ${name}${score}`); if (desc) lines.push(desc); if (hit.id) lines.push(` id: ${String(hit.id)}`); if (hit.ref) lines.push(` ref: ${String(hit.ref)}`); if (hit.origin !== undefined) lines.push(` origin: ${String(hit.origin)}`); if (Array.isArray(hit.keys) && hit.keys.length > 0) lines.push(` keys: ${hit.keys.join(", ")}`); if (hit.size) lines.push(` size: ${String(hit.size)}`); if (hit.action) lines.push(` action: ${String(hit.action)}`); if (hit.run) lines.push(` run: ${String(hit.run)}`); if (Array.isArray(hit.tags) && hit.tags.length > 0) lines.push(` tags: ${hit.tags.join(", ")}`); // Optional v1 spec §4.2 quality marker (e.g. "curated" / "proposed"). if (typeof hit.quality === "string" && hit.quality) lines.push(` quality: ${hit.quality}`); // Surface optional hit-level warnings (v1 spec §4.2). if (Array.isArray(hit.warnings) && hit.warnings.length > 0) { lines.push(` warnings: ${hit.warnings.join("; ")}`); } if (detail === "full") { if (hit.path) lines.push(` path: ${String(hit.path)}`); if (hit.editable != null) lines.push(` editable: ${String(hit.editable)}`); if (hit.editHint) lines.push(` editHint: ${String(hit.editHint)}`); const whyMatched = hit.whyMatched; if (whyMatched && whyMatched.length > 0) { lines.push(` whyMatched: ${whyMatched.join(", ")}`); } } lines.push(""); // blank line between hits } if (detail === "full" && r.timing) { const timing = r.timing; const parts = []; if (timing.totalMs != null) parts.push(`total: ${timing.totalMs}ms`); if (timing.rankMs != null) parts.push(`rank: ${timing.rankMs}ms`); if (timing.embedMs != null) parts.push(`embed: ${timing.embedMs}ms`); if (parts.length > 0) lines.push(`timing: ${parts.join(", ")}`); } // REC-02: When stash hits exist, tell the agent the next required step so it // doesn't skip `akm show` and write from training memory instead. if (hits.length >= 1) { // Prefer skill/command/agent type hits for the "Next:" ref — knowledge docs are // supplementary context, not the authoritative schema agents should load first. const preferredHit = hits.find((h) => h.type === "skill" || h.type === "command" || h.type === "agent") ?? hits[0]; const topRef = typeof preferredHit.ref === "string" ? preferredHit.ref : null; const hasWorkflowHit = hits.some((h) => h.type === "workflow"); if (topRef) { if (hasWorkflowHit) { const workflowRef = hits.find((h) => h.type === "workflow"); const wfRef = workflowRef && typeof workflowRef.ref === "string" ? workflowRef.ref : topRef; lines.push(`Next: akm show '${topRef}' | To execute a workflow: akm workflow run '${wfRef}'`); lines.push("Inspect the workflow before running it; `workflow run` executes and verifies its steps automatically."); } else { lines.push(`Next: akm show '${topRef}'`); lines.push("After reading the asset: check whether a workflow applies before editing — if so, inspect it and use `akm workflow run`."); } } } return lines.join("\n").trimEnd(); } export function formatCuratePlain(r, detail) { const query = typeof r.query === "string" ? r.query : ""; const summary = typeof r.summary === "string" ? r.summary : ""; const items = Array.isArray(r.items) ? r.items : []; const lines = [`Curated results for "${query}"`]; if (summary) lines.push(summary); if (items.length === 0) { if (r.tip) lines.push(String(r.tip)); return lines.join("\n"); } for (const item of items) { const type = typeof item.type === "string" ? item.type : "unknown"; const name = typeof item.name === "string" ? item.name : "unnamed"; lines.push(""); lines.push(`[${type}] ${name}`); if (item.description) lines.push(` ${String(item.description)}`); if (item.preview) lines.push(` preview: ${String(item.preview)}`); if (item.ref) lines.push(` ref: ${String(item.ref)}`); if (item.id) lines.push(` id: ${String(item.id)}`); if (Array.isArray(item.keys) && item.keys.length > 0) { lines.push(` keys: ${item.keys.join(", ")}`); } if (Array.isArray(item.parameters) && item.parameters.length > 0) { lines.push(` parameters: ${item.parameters.join(", ")}`); } if (item.run) lines.push(` run: ${String(item.run)}`); if (item.followUp) lines.push(` show: ${String(item.followUp)}`); if (Array.isArray(item.supportRefs) && item.supportRefs.length > 0) { for (const support of item.supportRefs) { if (!support.ref) continue; const label = typeof support.type === "string" ? `[${support.type}] ` : ""; const why = typeof support.reason === "string" ? ` — ${support.reason}` : ""; lines.push(` support: ${label}${String(support.ref)}${why}`); } } if (detail !== "brief" && item.reason) lines.push(` why: ${String(item.reason)}`); } const warnings = Array.isArray(r.warnings) ? r.warnings : []; if (warnings.length > 0) { lines.push(""); lines.push("Warnings:"); for (const warning of warnings) { lines.push(`- ${String(warning)}`); } } lines.push(""); lines.push("Next steps:"); lines.push(" 1. Run `akm show <ref>` for the best result above to read the full schema."); lines.push(" 2. Edit the workspace file using the schema field names and your task-specific values."); lines.push(" 3. Run `akm feedback <ref> --positive` when the task succeeds."); lines.push("To search further: akm search '<query>'"); return lines.join("\n"); } export function formatInitPlain(r) { let out = `Bundle initialized at ${r.bundleDir ?? "unknown"}`; // When --dir scaffolded a secondary bundle but the default was deliberately // left untouched, tell the user instead of silently repointing their default. if (r.defaultBundleUpdated === false && typeof r.previousBundleDir === "string" && r.previousBundleDir) { out += `\nYour default bundle is unchanged (${r.previousBundleDir}). Re-run with --set-default to make ${r.bundleDir} the default.`; } else if (r.configPath) { out += `\nConfig saved to ${r.configPath}`; } return out; } export function formatIndexPlain(r) { const indexResult = r; let out = `Indexed ${indexResult.totalEntries ?? 0} entries from ${indexResult.directoriesScanned ?? 0} directories (mode: ${indexResult.mode ?? "unknown"})`; const warnings = indexResult.warnings; if (Array.isArray(warnings) && warnings.length > 0) { out += `\nWarnings (${warnings.length}):`; for (const message of warnings) out += `\n - ${String(message)}`; } const verification = indexResult.verification; if (verification?.ok === false && verification.message) { out += `\nVerification: ${String(verification.message)}`; } const timing = indexResult.timing; if (timing) { out += `\nTiming: total ${timing.totalMs}ms` + `, preflight ${timing.preflightMs}ms` + `, walk ${timing.walkMs}ms` + `, llm ${timing.llmMs}ms` + `, embeddings ${timing.embedMs}ms` + `, fts ${timing.ftsMs}ms` + `, finalize ${timing.finalizeMs}ms` + `, clean ${timing.cleanMs}ms` + `, end-to-end ${timing.endToEndMs}ms`; } return out; } export function formatListPlain(r) { const sources = Array.isArray(r.sources) ? r.sources : []; if (sources.length === 0) return "No sources configured. Use `akm bundle add` to add a source."; const lines = []; for (const src of sources) { const kind = typeof src.kind === "string" ? src.kind : "unknown"; const name = typeof src.name === "string" ? src.name : "unnamed"; const ver = typeof src.version === "string" ? ` v${src.version}` : ""; const prov = typeof src.provider === "string" ? ` (${src.provider})` : ""; const flags = []; if (src.default === true) flags.push("default"); if (src.writable === true) flags.push("writable"); const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : ""; lines.push(`[${kind}] ${name}${ver}${prov}${flagText}`); } lines.push(""); lines.push("To search: akm search '<query>' | To view an asset: akm show <ref>"); return lines.join("\n"); } /** Render `akm models list`'s `{ rows }` as a column-aligned table (#946). */ export function formatModelsListPlain(r) { const rows = Array.isArray(r.rows) ? r.rows : []; if (rows.length === 0) return "No model aliases configured."; const headers = ["ALIAS", "COLUMN", "MODEL", "SOURCE", "VIA", "ENGINE"]; const cells = rows.map((row) => [ String(row.alias ?? "?"), String(row.column ?? "?"), String(row.model ?? "?"), String(row.source ?? "?"), String(row.via ?? "?"), row.engine !== undefined ? String(row.engine) : "-", ]); const widths = headers.map((header, i) => { let width = header.length; for (const cols of cells) { const cell = cols[i]; if (cell !== undefined && cell.length > width) width = cell.length; } return width; }); const renderRow = (cols) => cols .map((cell, i) => cell.padEnd(widths[i] ?? cell.length)) .join(" ") .trimEnd(); return [renderRow(headers), ...cells.map(renderRow)].join("\n"); } /** Render a single `SourceEntry` — `akm bundle show <name>`'s detail view of one `list` row. */ export function formatBundleShowPlain(r) { const name = typeof r.name === "string" ? r.name : "unknown"; const kind = typeof r.kind === "string" ? r.kind : "unknown"; const lines = [`${name} [${kind}]`]; if (r.default === true) lines.push(" default: true"); if (r.writable === true) lines.push(" writable: true"); if (typeof r.version === "string") lines.push(` version: ${r.version}`); if (typeof r.provider === "string") lines.push(` provider: ${r.provider}`); if (typeof r.path === "string") lines.push(` path: ${r.path}`); if (typeof r.ref === "string") lines.push(` ref: ${r.ref}`); if (typeof r.registryId === "string") lines.push(` registryId: ${r.registryId}`); lines.push(` items: ${typeof r.itemCount === "number" ? r.itemCount : 0}`); const byType = r.byType; if (byType && Object.keys(byType).length > 0) { lines.push(` byType: ${Object.entries(byType) .map(([t, n]) => `${t}=${n}`) .join(", ")}`); } const status = r.status; if (status?.exists === false) lines.push(" status: MISSING on disk"); return lines.join("\n"); } export function formatAddPlain(r) { // `akm bundle add <ref>` (source-add.ts `AddResponse`) and `akm bundle add // <target> --provider <kind>` (source-manage.ts `SourceAddResult`) are // genuinely different operations that happen to share the "add" command // name: the former eagerly fetches and indexes content, the latter only // writes a desired locator to config — nothing is synced until a later // `akm bundle update`. Force-fitting the declarative shape's `added`/`entry`/`message` // fields into the "Installed <ref> (N scanned...)" wording produced // "Installed undefined (0 directories scanned...)" (R-014) because it // implied a sync that never happened. `AddResponse` always carries an // `index` block (even when zero entries were scanned); `SourceAddResult` // never does — that is the reliable shape discriminator used below to // render each honestly instead of unifying them. if (!("index" in r)) return formatDeclarativeAddPlain(r); const index = r.index; const scanned = index?.directoriesScanned ?? 0; const total = index?.totalEntries ?? 0; const bundleSuffix = typeof r.bundleId === "string" && r.bundleId.length > 0 ? ` as bundle "${r.bundleId}"` : ""; const lines = [`Installed ${r.ref}${bundleSuffix} (${scanned} directories scanned, ${total} total assets indexed)`]; const warnings = index?.warnings; if (Array.isArray(warnings) && warnings.length > 0) { lines.push(`Warnings (${warnings.length}):`); for (const message of warnings) lines.push(` - ${String(message)}`); } return lines.join("\n"); } /** Render a `SourceAddResult` (declarative `--provider` add) — see {@link formatAddPlain}. */ function formatDeclarativeAddPlain(r) { if (r.added !== true) { return typeof r.message === "string" ? r.message : "add: no changes"; } const entry = r.entry; const kind = typeof entry?.type === "string" ? entry.type : "source"; const name = (typeof entry?.name === "string" && entry.name) || (typeof entry?.path === "string" && entry.path) || (typeof entry?.url === "string" && entry.url) || "source"; // A filesystem bundle reflects files already on disk — nothing to fetch, // just index it. Every other declarative kind (git/website/npm) is a // locator only; its content is not materialized until `akm bundle update`. if (kind === "filesystem") { return `Added ${name} (filesystem) — run \`akm index\` to index it.`; } return `Added ${name} (${kind}) — not yet synced; run \`akm bundle update ${name}\` to fetch it.`; } export function formatRemovePlain(r) { const target = r.target ?? r.ref ?? ""; const ok = r.ok !== false ? "OK" : "FAILED"; return `remove: ${target} ${ok}`; } export function formatBundleRenamePlain(r) { const oldId = String(r.oldId ?? "?"); const newId = String(r.newId ?? "?"); const applied = r.applied === true; const config = r.config ?? {}; const state = r.state ?? {}; const index = r.index ?? {}; const schedulerRefs = Array.isArray(config.schedulerRefs) ? config.schedulerRefs.length : 0; const proposalRefs = Number(state.proposalRefs ?? 0); const proposalTargets = Number(state.proposalTargets ?? 0); const taskHistoryRefs = Number(state.taskHistoryRefs ?? 0); const entries = Number(index.entries ?? 0); const contentRefs = Array.isArray(r.contentRefs) ? r.contentRefs : []; const nativeSchedulerRows = Array.isArray(r.nativeSchedulerRows) ? r.nativeSchedulerRows : []; const verb = applied ? "Renamed" : "Would rename"; const lines = [ `${verb} bundle "${oldId}" to "${newId}" (${entries} index entries, ${schedulerRefs} scheduler ref(s), ` + `${proposalRefs + proposalTargets} proposal reference(s), ${taskHistoryRefs} task-history row(s)).`, ]; if (!applied) { lines.push("Re-run without --dry-run to apply. It also re-syncs native scheduler bindings under the new name."); if (nativeSchedulerRows.length > 0) { lines.push(`Native scheduler rows naming "${oldId}//" that the sync would replace:`); for (const row of nativeSchedulerRows) lines.push(` - ${String(row)}`); } } else { const taskSync = r.taskSync; const syncResult = taskSync?.result; if (syncResult) { const installed = Array.isArray(syncResult.installed) ? syncResult.installed.length : 0; const updated = Array.isArray(syncResult.updated) ? syncResult.updated.length : 0; const removed = Array.isArray(syncResult.removed) ? syncResult.removed.length : 0; const failures = Array.isArray(syncResult.failures) ? syncResult.failures : []; if (failures.length === 0) { lines.push(`Native scheduler bindings re-synced under the new name (${installed} installed, ${updated} updated, ${removed} removed).`); } else { lines.push(`Native scheduler sync after rename: ${installed} installed, ${updated} updated, ${removed} removed, ${failures.length} failed.`); for (const failure of failures) lines.push(` - ${failure.ref ?? failure.path}: ${failure.reason}`); lines.push("Run `akm task sync` to retry."); } } else if (taskSync?.ok === false && taskSync.error) { lines.push(`Re-syncing native scheduler bindings failed: ${taskSync.error}. Run \`akm task sync\` to retry.`); } } if (contentRefs.length > 0) { lines.push(`Content still spelling "${oldId}//" (not rewritten — edit these by hand):`); for (const file of contentRefs) lines.push(` - ${String(file)}`); } return lines.join("\n"); } export function formatUpdatePlain(r) { // R-015: `processed` alone conflated three cases into one empty array — // a true no-op (--all with nothing configured), a source this call flatly // never looked at (a plain source under --all), and a plain git/website // source that WAS just synced successfully but has no `UpdateResultItem` // shape to report (no version/lock to diff). All three rendered as the // same misleading "nothing to update". `plainSynced` and `skipped` (see // sources/types.ts) cover the other two so this can report accurately. const processed = r.processed; const plainSynced = r.plainSynced; const skipped = r.skipped; const lines = []; for (const item of processed ?? []) { const changed = item.changed; const installed = item.installed; const previous = item.previous; if (changed?.any) { const prev = previous?.resolvedVersion ?? "unknown"; const next = installed?.resolvedVersion ?? "unknown"; lines.push(`update: ${item.id} v${prev} → v${next}`); } else { lines.push(`update: ${item.id} (unchanged)`); } } for (const item of plainSynced ?? []) { lines.push(item.kind === "filesystem" ? `update: ${item.id} reconciled (filesystem)` : `update: ${item.id} synced (${item.kind})`); } for (const item of skipped ?? []) { lines.push(`update: ${item.id} skipped — ${item.reason}`); } return lines.length > 0 ? lines.join("\n") : `update: nothing to update`; } export function formatUpgradePlain(r) { if (r.upgraded === true) { return `akm upgraded: v${r.currentVersion} → v${r.newVersion}`; } if (r.updateAvailable === true) { return `akm v${r.currentVersion} → v${r.latestVersion} available (run 'akm upgrade' to install)`; } if (r.updateAvailable === false && r.latestVersion) { return `akm v${r.currentVersion} is already the latest version`; } if (r.message) return String(r.message); return null; } export function formatClonePlain(r) { const dst = r.destination?.path ?? "unknown"; const remote = r.remoteFetched ? " (fetched from remote)" : ""; const over = r.overwritten ? " (overwritten)" : ""; return `Cloned${remote} → ${dst}${over}`; }