@brianlovin/notion-skills
Version:
Sync agent skills from a Notion database to Claude Code, Codex, OpenCode, Cursor, Gemini CLI.
80 lines • 3.08 kB
JavaScript
import chalk from "chalk";
import { Separator, checkbox } from "@inquirer/prompts";
import { dirname } from "node:path";
/**
* Render a multiselect of local skills not yet in Notion, grouped by
* source directory. Returns the slugs the user kept (empty array if
* they deselected everything).
*
* Used by `init` after it discovers skills on disk that aren't
* represented in the configured database. `import` skips this picker
* and confirms in bulk; `sync` is pull-only and never uploads.
*/
export async function pickLocalSkillsToUpload(skills) {
if (skills.length === 0)
return [];
const byKey = new Map();
for (const s of skills) {
const dirs = [s.sourceDisplay, ...(s.additionalSourceDisplays ?? [])]
.map((p) => homeRelative(dirname(p)))
.sort();
const key = dirs.join("\n");
let g = byKey.get(key);
if (!g) {
g = { label: dirs.join(", "), skills: [] };
byKey.set(key, g);
}
g.skills.push(s);
}
const groups = [...byKey.values()];
for (const g of groups)
g.skills.sort((a, b) => a.name.localeCompare(b.name));
const total = skills.length;
const message = total === 1
? `Found 1 local skill not in Notion. Upload it?`
: `Found ${total} local skills not in Notion. Pick which to upload:`;
const cols = process.stdout.columns ?? 100;
const longestName = Math.max(...skills.map((s) => s.name.length));
// pad so descriptions line up; cap at 32 so a wildly long name doesn't
// squeeze the description column to nothing.
const namePad = Math.min(32, longestName + 2);
const descMax = Math.max(20, cols - namePad - 10);
const choices = [];
const showHeaders = groups.length > 1;
for (const g of groups) {
if (showHeaders) {
choices.push(new Separator(chalk.dim(` ${g.label}`)));
}
for (const skill of g.skills) {
const desc = truncate(oneLine(skill.description), descMax);
const name = `${skill.name.padEnd(namePad)} ${chalk.dim(desc)}`;
choices.push({ name, value: skill.name, checked: true });
}
}
return checkbox({
message,
choices,
pageSize: Math.min(25, choices.length + 1),
// After submit, inquirer echoes the selected values back on the prompt
// line. With 17+ skill names that wraps into a paragraph and clutters
// the transcript. Replace it with a compact "(N of M selected)" line.
theme: {
style: {
renderSelectedChoices: (selected) => chalk.dim(`(${selected.length} of ${total} selected)`),
},
},
});
}
function homeRelative(p) {
const home = process.env.HOME;
return home && p.startsWith(home) ? "~" + p.slice(home.length) : p;
}
function oneLine(s) {
return s.replace(/\s+/g, " ").trim();
}
function truncate(s, max) {
if (s.length <= max)
return s;
return s.slice(0, max - 3).trimEnd() + "...";
}
//# sourceMappingURL=_pick-locals.js.map