feat: add Polytoken installer support

This commit is contained in:
Scott Jennings
2026-07-11 13:19:27 -05:00
parent 49069b8b52
commit 63be1bfc26
3 changed files with 107 additions and 2 deletions
+61
View File
@@ -1400,6 +1400,67 @@ async function runTests() {
console.log('');
// ============================================================
// Suite 28b: Polytoken Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 28b: Polytoken Native Skills${colors.reset}\n`);
let tempProjectDir28b;
let installedBmadDir28b;
try {
clearCache();
const platformCodes28b = await loadPlatformCodes();
const polytokenInstaller = platformCodes28b.platforms.polytoken?.installer;
assert(polytokenInstaller?.target_dir === '.polytoken/skills', 'Polytoken target_dir uses its project-local native skills path');
assert(!polytokenInstaller?.global_target_dir, 'Polytoken does not claim an undocumented global skills path');
tempProjectDir28b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-polytoken-test-'));
installedBmadDir28b = await createTestBmadFixture();
const ideManager28b = new IdeManager();
await ideManager28b.ensureInitialized();
assert(
ideManager28b.getAvailableIdes().some((ide) => ide.value === 'polytoken'),
'Polytoken appears in available IDEs list',
);
const detectedBefore28b = await ideManager28b.detectInstalledIdes(tempProjectDir28b);
assert(!detectedBefore28b.includes('polytoken'), 'Polytoken is not detected before install');
const result28b = await ideManager28b.setup('polytoken', tempProjectDir28b, installedBmadDir28b, {
silent: true,
selectedModules: ['bmm'],
});
assert(result28b.success === true, 'Polytoken setup succeeds against temp project');
const detectedAfter28b = await ideManager28b.detectInstalledIdes(tempProjectDir28b);
assert(detectedAfter28b.includes('polytoken'), 'Polytoken is detected after install');
const skillFile28b = path.join(tempProjectDir28b, '.polytoken', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile28b), 'Polytoken install writes SKILL.md directory output');
const skillContent28b = await fs.readFile(skillFile28b, 'utf8');
const frontmatter28b = skillContent28b.match(/^---\n([\s\S]*?)\n---\n?/);
assert(frontmatter28b, 'Polytoken SKILL.md contains valid frontmatter delimiters');
assert(/^description:\s*\S.+$/m.test(frontmatter28b?.[1] || ''), 'Polytoken skill has a non-empty description');
assert(skillContent28b.includes('agent-activation'), 'Polytoken skill preserves its activation instructions');
const reinstall28b = await ideManager28b.setup('polytoken', tempProjectDir28b, installedBmadDir28b, {
silent: true,
selectedModules: ['bmm'],
});
assert(reinstall28b.success === true, 'Polytoken reinstall succeeds over existing skills');
} catch (error) {
assert(false, 'Polytoken native skills test succeeds', error.message);
} finally {
if (tempProjectDir28b) await fs.remove(tempProjectDir28b).catch(() => {});
if (installedBmadDir28b) await fs.remove(path.dirname(installedBmadDir28b)).catch(() => {});
}
console.log('');
// ============================================================
// Suite 29: Unified Skill Scanner — collectSkills
// ============================================================
+6
View File
@@ -281,6 +281,12 @@ platforms:
target_dir: .agents/skills
global_target_dir: ~/.agents/skills
polytoken:
name: "Polytoken"
preferred: false
installer:
target_dir: .polytoken/skills
qoder:
name: "Qoder"
preferred: false
+40 -2
View File
@@ -266,6 +266,19 @@ async function autocompleteMultiselect(options) {
const filterFn = options.filter ?? defaultAutocompleteFilter;
const lockedSet = new Set(options.lockedValues || []);
if (typeof core.AutocompletePrompt !== 'function') {
const initialValues = [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])];
const result = await clack.multiselect({
message: options.message,
options: options.options,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: options.required || false,
maxItems: options.maxItems,
});
await handleCancel(result);
return [...new Set([...result, ...(options.lockedValues || [])])];
}
const prompt = new core.AutocompletePrompt({
options: options.options,
multiple: true,
@@ -558,7 +571,17 @@ async function cancel(message = 'Operation cancelled') {
*/
async function box(content, title, options) {
const clack = await getClack();
clack.box(content, title, options);
// @clack/prompts does not currently expose box(), despite some versions of
// its documentation and ecosystem examples referring to one. Keep the
// wrapper forward-compatible while falling back to Clack's supported boxed
// note renderer for current releases.
if (typeof clack.box === 'function') {
clack.box(content, title, options);
return;
}
clack.note(content, title);
}
/**
@@ -573,7 +596,8 @@ async function box(content, title, options) {
*/
async function autocomplete(options) {
const clack = await getClack();
const result = await clack.autocomplete(options);
const prompt = typeof clack.autocomplete === 'function' ? clack.autocomplete : clack.select;
const result = await prompt(options);
await handleCancel(result);
return result;
}
@@ -654,6 +678,20 @@ function listDirectoryOptions(input, options) {
*/
async function directory(options) {
const core = await getClackCore();
if (typeof core.AutocompletePrompt !== 'function') {
const clack = await getClack();
const result = await clack.text({
message: options.message,
placeholder: options.placeholder,
initialValue: options.default,
defaultValue: options.default,
validate: options.validate,
});
await handleCancel(result);
return result;
}
const color = await getPicocolors();
const tabCompletion = {
prefix: '',