mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Sync generated provider output
This commit is contained in:
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
Call skill({ name: "impeccable" }) and follow its `Setup` and `Commands` sections to handle $ARGUMENTS.
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
|
||||
import { basename, join, resolve, dirname } from 'node:path';
|
||||
import { basename, join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -115,21 +116,119 @@ Invoke ${commandPrefix}impeccable ${command}, passing along any arguments provid
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode 1.18.10 does not honor `user-invocable: true` on SKILL.md frontmatter
|
||||
// (see docs/HARNESSES.md and opencode/packages/core/src/v1/config/command.ts),
|
||||
// so a pinned skill there shows up in `opencode debug skill` but never in the
|
||||
// slash menu. The fix is a sibling `commands/impeccable-<cmd>.md` that uses the
|
||||
// OpenCode command schema (description, agent, subtask). Body loads the skill
|
||||
// via the skill tool and then the sub-command's reference file directly, so
|
||||
// /impeccable-<cmd> runs the same workflow /impeccable <cmd> routes to.
|
||||
const OPENCODE_PIN_MARKER = '<!-- impeccable-pinned-command -->';
|
||||
function generatePinnedOpencodeCommand(command, metadata) {
|
||||
const desc = metadata[command]?.description || `Impeccable sub-command shortcut; runs the ${command} workflow via /impeccable.`;
|
||||
return `---
|
||||
description: "${desc}"
|
||||
agent: build
|
||||
subtask: true
|
||||
---
|
||||
|
||||
${OPENCODE_PIN_MARKER}
|
||||
|
||||
Load the \`impeccable\` skill via the skill tool (name: "impeccable"), then run \`node <skill-base-dir>/scripts/context.mjs\`, then load \`<skill-base-dir>/reference/${command}.md\` and follow it. \`<skill-base-dir>\` is the skill's base directory as reported by the skill tool response; substitute the actual absolute path before running or reading anything.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
}
|
||||
|
||||
// OpenCode's user-scope config dir. Mirrors the CLI's opencodeGlobalConfigDir
|
||||
// precedence (OPENCODE_CONFIG_DIR → XDG_CONFIG_HOME/opencode →
|
||||
// ~/.config/opencode); duplicated here because this script ships inside the
|
||||
// installed skill and cannot import the CLI.
|
||||
function opencodeUserConfigDir() {
|
||||
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
||||
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, 'opencode');
|
||||
return join(homedir(), '.config', 'opencode');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve every commands dir that should receive an OpenCode pin: the
|
||||
* project-local dir when the project has the skill, plus the user config dir
|
||||
* when Impeccable is installed globally (#406 layout). A user-scope skill is
|
||||
* visible from every project, so its pinned commands belong next to it.
|
||||
* With `forCleanup`, both commands dirs are included even when the skill is
|
||||
* gone, so unpin can still reach a pin left behind by a removed install;
|
||||
* removal stays safe because removePinnedOpencodeCommand is marker-guarded.
|
||||
*/
|
||||
function findOpencodeCommandsDirs(projectRoot, { forCleanup = false } = {}) {
|
||||
const dirs = [];
|
||||
const seen = new Set();
|
||||
const push = (commandsDir) => {
|
||||
const key = resolve(commandsDir);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
dirs.push(commandsDir);
|
||||
}
|
||||
};
|
||||
if (forCleanup || existsSync(join(projectRoot, '.opencode', 'skills', 'impeccable'))) {
|
||||
push(join(projectRoot, '.opencode', 'commands'));
|
||||
}
|
||||
const userConfig = opencodeUserConfigDir();
|
||||
if (forCleanup || existsSync(join(userConfig, 'skills', 'impeccable'))) {
|
||||
push(join(userConfig, 'commands'));
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function writePinnedOpencodeCommand(commandsDir, command, metadata) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (existsSync(commandFile)) {
|
||||
const existing = readFileSync(commandFile, 'utf-8');
|
||||
if (!existing.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (non-pinned command already exists)`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mkdirSync(commandsDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(commandFile, generatePinnedOpencodeCommand(command, metadata));
|
||||
console.log(` + ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function removePinnedOpencodeCommand(commandsDir, command) {
|
||||
const commandFile = join(commandsDir, `impeccable-${command}.md`);
|
||||
if (!existsSync(commandFile)) return false;
|
||||
const content = readFileSync(commandFile, 'utf-8');
|
||||
if (!content.includes(OPENCODE_PIN_MARKER)) {
|
||||
console.log(` SKIP: ${commandFile} (not a pinned command)`);
|
||||
return false;
|
||||
}
|
||||
rmSync(commandFile, { force: true });
|
||||
console.log(` - ${commandFile}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a command: create shortcut skill in all harness dirs.
|
||||
*/
|
||||
function pin(command, projectRoot) {
|
||||
const metadata = loadCommandMetadata();
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
const opencodeCommandsDirs = findOpencodeCommandsDirs(projectRoot);
|
||||
|
||||
if (harnessDirs.length === 0) {
|
||||
if (harnessDirs.length === 0 && opencodeCommandsDirs.length === 0) {
|
||||
console.log('No harness directories with impeccable installed found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
let created = 0;
|
||||
|
||||
// OpenCode is handled separately below because its shortcut format is a
|
||||
// slash command, not a SKILL.md. Excluding it from the skill loop here
|
||||
// prevents a duplicate `.opencode/skills/<cmd>/SKILL.md` that OpenCode
|
||||
// would never surface as `/<cmd>`.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const commandPrefix = commandPrefixForSkillsDir(skillsDir);
|
||||
const content = generatePinnedSkill(command, metadata, commandPrefix, commandPrefix === '$');
|
||||
// Check if skill already exists (and isn't a pin)
|
||||
@@ -151,6 +250,12 @@ function pin(command, projectRoot) {
|
||||
created++;
|
||||
}
|
||||
|
||||
// OpenCode: write a slash command bridge, not a skill shortcut. Covers both
|
||||
// project installs and user-scope (global config) installs.
|
||||
for (const commandsDir of opencodeCommandsDirs) {
|
||||
if (writePinnedOpencodeCommand(commandsDir, command, metadata)) created++;
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
|
||||
console.log('Use the pinned command directly in each harness.');
|
||||
@@ -160,13 +265,17 @@ function pin(command, projectRoot) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a command: remove shortcut skill from all harness dirs.
|
||||
* Unpin a command: remove shortcut skill in all harness dirs.
|
||||
*/
|
||||
function unpin(command, projectRoot) {
|
||||
const harnessDirs = findHarnessDirs(projectRoot);
|
||||
let removed = 0;
|
||||
|
||||
// OpenCode has its own cleanup path below; skip the skill loop here so a
|
||||
// stray `.opencode/skills/<cmd>/SKILL.md` written by an older Impeccable
|
||||
// version is never silently dropped here.
|
||||
for (const skillsDir of harnessDirs) {
|
||||
if (skillsDir.includes(`${sep}.opencode${sep}`)) continue;
|
||||
const skillDir = join(skillsDir, command);
|
||||
if (!existsSync(skillDir)) continue;
|
||||
|
||||
@@ -185,6 +294,13 @@ function unpin(command, projectRoot) {
|
||||
removed++;
|
||||
}
|
||||
|
||||
// OpenCode: remove the pinned command file if it's one of ours, in every
|
||||
// scope it could have been written to — even when the skill itself is
|
||||
// already gone, since removal is marker-guarded.
|
||||
for (const commandsDir of findOpencodeCommandsDirs(projectRoot, { forCleanup: true })) {
|
||||
if (removePinnedOpencodeCommand(commandsDir, command)) removed++;
|
||||
}
|
||||
|
||||
if (removed > 0) {
|
||||
console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
|
||||
console.log(`Use Impeccable's '${command}' workflow directly to access it.`);
|
||||
|
||||
Reference in New Issue
Block a user