developer-stack-skills
Advanced tools
| #!/usr/bin/env node | ||
| const { runPostInstall } = require("../lib/installer"); | ||
| runPostInstall().catch((error) => { | ||
| console.error(`[developer-stack-skills] postinstall failed: ${error.message}`); | ||
| process.exitCode = 1; | ||
| }); |
| # Release Notes | ||
| ## 1.2.0 - 2026-05-15 | ||
| This release streamlines package setup so installing package can immediately configure skills without extra manual steps in interactive environments. | ||
| Highlights: | ||
| - `npm install developer-stack-skills` now sets up project-level skills automatically | ||
| - `npm install -g developer-stack-skills` now sets up globally installed skills under `~/.ai-skills/developer-stack-skills/` | ||
| - added `uninstall`, `version`, and `help` commands | ||
| - added `--dry-run` for install and uninstall preview | ||
| - source checkout and CI/non-interactive installs skip auto-config safely | ||
| Behavior details: | ||
| - local package installs default to project-level skill installation and prefer `copy` | ||
| - global package installs default to global skill installation and prefer `symlink` | ||
| - agent integration still happens by updating agent config files with skill paths | ||
| - uninstall removes managed config entries and installed skill folders | ||
| Manual fallback remains available: | ||
| ```bash | ||
| developer-stack-skills install | ||
| npx developer-stack-skills install | ||
| ``` |
| #!/usr/bin/env node | ||
| const { parseArgs, printHelp, runInstall } = require("../lib/installer"); | ||
| const { | ||
| parseArgs, | ||
| printHelp, | ||
| printVersion, | ||
| runInstall, | ||
| runUninstall, | ||
| } = require("../lib/installer"); | ||
@@ -13,2 +19,17 @@ async function main() { | ||
| if (args.command === "uninstall") { | ||
| await runUninstall(args); | ||
| return; | ||
| } | ||
| if (["version", "--version", "-v"].includes(args.command)) { | ||
| printVersion(); | ||
| return; | ||
| } | ||
| if (["help", "--help", "-h"].includes(args.command)) { | ||
| printHelp(); | ||
| return; | ||
| } | ||
| printHelp(); | ||
@@ -18,4 +39,4 @@ } | ||
| main().catch((error) => { | ||
| console.error(`[developer-stack-skills] install failed: ${error.message}`); | ||
| console.error(`[developer-stack-skills] command failed: ${error.message}`); | ||
| process.exitCode = 1; | ||
| }); |
+30
-1
| # Changelog | ||
| ## 1.2.0 - 2026-05-15 | ||
| Added: | ||
| - `postinstall` hook to auto-run configuration during interactive `npm install` | ||
| - `source` package install type detection to skip auto-config when working inside repo checkout | ||
| - global skill install target at `~/.ai-skills/developer-stack-skills/` | ||
| - default install mode selection by package scope: `copy` for local install, `symlink` for global install | ||
| - `uninstall` command to remove installed skills and agent config entries | ||
| - `version` command | ||
| - `help` command | ||
| - `--dry-run` flag for install and uninstall preview | ||
| - test coverage for install scope helpers, `source` detection, and uninstall config cleanup helpers | ||
| Changed: | ||
| - `npm install developer-stack-skills` now configures project-level skills without separate installer step | ||
| - `npm install -g developer-stack-skills` now configures global skill install without separate installer step | ||
| - global install flow still prompts for project directory to update agent config files | ||
| - CLI now supports full command lifecycle: install, uninstall, version, and help | ||
| - help output now documents commands and dry-run usage | ||
| - README now documents auto-config behavior, local vs global install scope, uninstall/version/help commands, and updated example logs | ||
| Notes: | ||
| - auto-config skips in non-interactive environments such as CI | ||
| - uninstall removes agent linkage by rewriting config files, not by agent symlink removal | ||
| - manual fallback remains available with `developer-stack-skills install` or `npx developer-stack-skills install` | ||
| ## 1.1.0 - 2026-05-15 | ||
@@ -23,2 +52,2 @@ | ||
| - installer is manual by design; it does not auto-run during `npm install` | ||
| - installer originally required manual execution after package install |
+376
-51
| const fsp = require("fs/promises"); | ||
| const os = require("os"); | ||
| const path = require("path"); | ||
@@ -37,2 +38,3 @@ const readline = require("readline"); | ||
| projectDir: null, | ||
| dryRun: false, | ||
| yes: false, | ||
@@ -49,2 +51,7 @@ }; | ||
| if (token === "--dry-run") { | ||
| args.dryRun = true; | ||
| continue; | ||
| } | ||
| if (token.startsWith("--agent=")) { | ||
@@ -110,5 +117,14 @@ args.agent = token.slice("--agent=".length); | ||
| console.log(" developer-stack-skills install"); | ||
| console.log(" developer-stack-skills uninstall"); | ||
| console.log(" developer-stack-skills version"); | ||
| console.log(" developer-stack-skills help"); | ||
| console.log(" developer-stack-skills install --agent all --mode symlink --dir ."); | ||
| console.log(" npx developer-stack-skills install --agent cline --mode copy"); | ||
| console.log(" npx developer-stack-skills install --agent cline --mode copy --dry-run"); | ||
| console.log(""); | ||
| console.log("Commands:"); | ||
| console.log(" install install skills and update agent config"); | ||
| console.log(" uninstall remove installed skills and agent config entries"); | ||
| console.log(" version print package version"); | ||
| console.log(" help print this help"); | ||
| console.log(""); | ||
| console.log("Options:"); | ||
@@ -118,5 +134,10 @@ console.log(" --agent <all|claude|cursor|cline|roocode|copilot>"); | ||
| console.log(" --dir <project-directory>"); | ||
| console.log(" --dry-run"); | ||
| console.log(" --yes"); | ||
| } | ||
| function printVersion() { | ||
| console.log(getVersion()); | ||
| } | ||
| function createPrompt() { | ||
@@ -166,11 +187,36 @@ const rl = readline.createInterface({ | ||
| const normalizedPackageRoot = path.resolve(packageRoot); | ||
| const normalizedProjectDir = path.resolve(projectDir); | ||
| const localNodeModulesRoot = path.resolve(projectDir, "node_modules", PACKAGE_NAME); | ||
| if (normalizedPackageRoot === normalizedProjectDir) { | ||
| return "source"; | ||
| } | ||
| return normalizedPackageRoot === localNodeModulesRoot ? "local" : "global"; | ||
| } | ||
| function getInstallRoot(projectDir) { | ||
| function getInstallRoot(projectDir, packageInstallType) { | ||
| if (packageInstallType === "global") { | ||
| return path.join(os.homedir(), ".ai-skills", PACKAGE_NAME); | ||
| } | ||
| return path.join(projectDir, ".ai-skills", PACKAGE_NAME); | ||
| } | ||
| function getDefaultProjectDir(env = process.env, cwd = process.cwd()) { | ||
| return path.resolve(env.INIT_CWD || cwd); | ||
| } | ||
| function getDefaultMode(packageInstallType) { | ||
| return packageInstallType === "local" ? "copy" : "symlink"; | ||
| } | ||
| function isInteractiveInstall(env = process.env) { | ||
| if (env.DEVELOPER_STACK_SKILLS_SKIP_POSTINSTALL === "1") { | ||
| return false; | ||
| } | ||
| return Boolean(process.stdin.isTTY && process.stdout.isTTY && env.CI !== "true"); | ||
| } | ||
| function getSkillSourcePath(packageRoot, skillName) { | ||
@@ -184,21 +230,31 @@ return path.join(packageRoot, skillName); | ||
| async function ensureDir(dirPath) { | ||
| async function ensureDir(dirPath, dryRun = false) { | ||
| if (dryRun) { | ||
| return; | ||
| } | ||
| await fsp.mkdir(dirPath, { recursive: true }); | ||
| } | ||
| async function removePath(targetPath) { | ||
| async function removePath(targetPath, dryRun = false) { | ||
| if (dryRun) { | ||
| return; | ||
| } | ||
| await fsp.rm(targetPath, { recursive: true, force: true }); | ||
| } | ||
| async function installSkill({ packageRoot, installRoot, skillName, mode, platform }) { | ||
| async function installSkill({ packageRoot, installRoot, skillName, mode, platform, dryRun = false }) { | ||
| const sourcePath = getSkillSourcePath(packageRoot, skillName); | ||
| const destPath = getSkillDestPath(installRoot, skillName); | ||
| await removePath(destPath); | ||
| await removePath(destPath, dryRun); | ||
| if (mode === "copy") { | ||
| await fsp.cp(sourcePath, destPath, { recursive: true }); | ||
| if (!dryRun) { | ||
| await fsp.cp(sourcePath, destPath, { recursive: true }); | ||
| } | ||
| } else { | ||
| const symlinkType = platform === "windows" ? "junction" : "dir"; | ||
| await fsp.symlink(sourcePath, destPath, symlinkType); | ||
| if (!dryRun) { | ||
| await fsp.symlink(sourcePath, destPath, symlinkType); | ||
| } | ||
| } | ||
@@ -245,2 +301,16 @@ | ||
| function removeManagedBlock(content, commentStyle) { | ||
| const startMarker = commentStyle === "html" | ||
| ? `<!-- ${MANAGED_START} -->` | ||
| : `# ${MANAGED_START}`; | ||
| const endMarker = commentStyle === "html" | ||
| ? `<!-- ${MANAGED_END} -->` | ||
| : `# ${MANAGED_END}`; | ||
| const escapedStart = escapeRegExp(startMarker); | ||
| const escapedEnd = escapeRegExp(endMarker); | ||
| const pattern = new RegExp(`\\n?${escapedStart}[\\s\\S]*?${escapedEnd}\\n?`, "m"); | ||
| return content.replace(pattern, "").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, content.trim() ? "\n" : ""); | ||
| } | ||
| function escapeRegExp(value) { | ||
@@ -282,4 +352,50 @@ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| async function writeFileWithDirs(filePath, content) { | ||
| await ensureDir(path.dirname(filePath)); | ||
| function removeSkillsSectionItems(content, items, itemRenderer) { | ||
| const lines = content ? content.split(/\r?\n/) : []; | ||
| const sectionStart = lines.findIndex((line) => /^skills:\s*$/.test(line.trim())); | ||
| if (sectionStart === -1) { | ||
| return content; | ||
| } | ||
| let sectionEnd = sectionStart + 1; | ||
| while (sectionEnd < lines.length) { | ||
| const line = lines[sectionEnd]; | ||
| if (!line.trim()) { | ||
| sectionEnd += 1; | ||
| continue; | ||
| } | ||
| if (/^\s*-/.test(line) || /^\s*#/.test(line)) { | ||
| sectionEnd += 1; | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| const removeSet = new Set(items.map(itemRenderer)); | ||
| const keptLines = lines | ||
| .slice(sectionStart + 1, sectionEnd) | ||
| .filter((line) => !removeSet.has(line)); | ||
| const hasSkillEntries = keptLines.some((line) => /^\s*-/.test(line)); | ||
| const merged = hasSkillEntries | ||
| ? [ | ||
| ...lines.slice(0, sectionStart), | ||
| "skills:", | ||
| ...keptLines, | ||
| ...lines.slice(sectionEnd), | ||
| ] | ||
| : [ | ||
| ...lines.slice(0, sectionStart), | ||
| ...lines.slice(sectionEnd), | ||
| ]; | ||
| return `${merged.join("\n").replace(/\s*$/, "")}${merged.some((line) => line.trim()) ? "\n" : ""}`; | ||
| } | ||
| async function writeFileWithDirs(filePath, content, dryRun = false) { | ||
| await ensureDir(path.dirname(filePath), dryRun); | ||
| if (dryRun) { | ||
| return; | ||
| } | ||
| await fsp.writeFile(filePath, content, "utf8"); | ||
@@ -299,3 +415,3 @@ } | ||
| async function configureClaude(projectDir, skillPaths) { | ||
| async function configureClaude(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, "CLAUDE.md"); | ||
@@ -312,7 +428,7 @@ const current = await readIfExists(filePath); | ||
| const next = replaceManagedBlock(current, body, "html"); | ||
| await writeFileWithDirs(filePath, next); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| return filePath; | ||
| } | ||
| async function configureCursor(projectDir, skillPaths) { | ||
| async function configureCursor(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".cursor", "rules", "developer-stack-skills.mdc"); | ||
@@ -331,7 +447,7 @@ const body = [ | ||
| await writeFileWithDirs(filePath, body); | ||
| await writeFileWithDirs(filePath, body, dryRun); | ||
| return filePath; | ||
| } | ||
| async function configureCline(projectDir, skillPaths) { | ||
| async function configureCline(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".clinerules"); | ||
@@ -341,7 +457,7 @@ const current = await readIfExists(filePath); | ||
| await writeFileWithDirs(filePath, next); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| return filePath; | ||
| } | ||
| async function configureRoocode(projectDir, skillPaths) { | ||
| async function configureRoocode(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".roo", "config.yml"); | ||
@@ -351,7 +467,7 @@ const current = await readIfExists(filePath); | ||
| await writeFileWithDirs(filePath, next); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| return filePath; | ||
| } | ||
| async function configureCopilot(projectDir, skillPaths) { | ||
| async function configureCopilot(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".github", "copilot-instructions.md"); | ||
@@ -366,3 +482,3 @@ const current = await readIfExists(filePath); | ||
| const next = replaceManagedBlock(current, body, "html"); | ||
| await writeFileWithDirs(filePath, next); | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| return filePath; | ||
@@ -375,3 +491,3 @@ } | ||
| async function configureAgents(agent, projectDir, installRoot) { | ||
| async function configureAgents(agent, projectDir, installRoot, dryRun = false) { | ||
| const skillPaths = buildSkillPaths(installRoot); | ||
@@ -383,3 +499,3 @@ const targets = getAgentTargets(agent); | ||
| if (target === "claude") { | ||
| configured.push({ agent: target, filePath: await configureClaude(projectDir, skillPaths) }); | ||
| configured.push({ agent: target, filePath: await configureClaude(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
@@ -389,3 +505,3 @@ } | ||
| if (target === "cursor") { | ||
| configured.push({ agent: target, filePath: await configureCursor(projectDir, skillPaths) }); | ||
| configured.push({ agent: target, filePath: await configureCursor(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
@@ -395,3 +511,3 @@ } | ||
| if (target === "cline") { | ||
| configured.push({ agent: target, filePath: await configureCline(projectDir, skillPaths) }); | ||
| configured.push({ agent: target, filePath: await configureCline(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
@@ -401,3 +517,3 @@ } | ||
| if (target === "roocode") { | ||
| configured.push({ agent: target, filePath: await configureRoocode(projectDir, skillPaths) }); | ||
| configured.push({ agent: target, filePath: await configureRoocode(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
@@ -407,3 +523,3 @@ } | ||
| if (target === "copilot") { | ||
| configured.push({ agent: target, filePath: await configureCopilot(projectDir, skillPaths) }); | ||
| configured.push({ agent: target, filePath: await configureCopilot(projectDir, skillPaths, dryRun) }); | ||
| } | ||
@@ -415,20 +531,125 @@ } | ||
| async function collectAnswers(args) { | ||
| async function unconfigureClaude(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, "CLAUDE.md"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeManagedBlock(current, "html"); | ||
| if (next.trim()) { | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| } else { | ||
| await removePath(filePath, dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function unconfigureCursor(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".cursor", "rules", "developer-stack-skills.mdc"); | ||
| await removePath(filePath, dryRun); | ||
| return filePath; | ||
| } | ||
| async function unconfigureCline(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".clinerules"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeSkillsSectionItems(current, skillPaths, (skillPath) => ` - ${quoteYamlString(skillPath)}`); | ||
| if (next.trim()) { | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| } else { | ||
| await removePath(filePath, dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function unconfigureRoocode(projectDir, skillPaths, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".roo", "config.yml"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeSkillsSectionItems(current, skillPaths, (skillPath) => ` - path: ${quoteYamlString(skillPath)}`); | ||
| if (next.trim()) { | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| } else { | ||
| await removePath(filePath, dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function unconfigureCopilot(projectDir, dryRun = false) { | ||
| const filePath = path.join(projectDir, ".github", "copilot-instructions.md"); | ||
| const current = await readIfExists(filePath); | ||
| const next = removeManagedBlock(current, "html"); | ||
| if (next.trim()) { | ||
| await writeFileWithDirs(filePath, next, dryRun); | ||
| } else { | ||
| await removePath(filePath, dryRun); | ||
| } | ||
| return filePath; | ||
| } | ||
| async function unconfigureAgents(agent, projectDir, installRoot, dryRun = false) { | ||
| const skillPaths = buildSkillPaths(installRoot); | ||
| const targets = getAgentTargets(agent); | ||
| const configured = []; | ||
| for (const target of targets) { | ||
| if (target === "claude") { | ||
| configured.push({ agent: target, filePath: await unconfigureClaude(projectDir, dryRun) }); | ||
| continue; | ||
| } | ||
| if (target === "cursor") { | ||
| configured.push({ agent: target, filePath: await unconfigureCursor(projectDir, dryRun) }); | ||
| continue; | ||
| } | ||
| if (target === "cline") { | ||
| configured.push({ agent: target, filePath: await unconfigureCline(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
| } | ||
| if (target === "roocode") { | ||
| configured.push({ agent: target, filePath: await unconfigureRoocode(projectDir, skillPaths, dryRun) }); | ||
| continue; | ||
| } | ||
| if (target === "copilot") { | ||
| configured.push({ agent: target, filePath: await unconfigureCopilot(projectDir, dryRun) }); | ||
| } | ||
| } | ||
| return configured; | ||
| } | ||
| async function collectAnswers(args, defaults = {}) { | ||
| const prompt = createPrompt(); | ||
| try { | ||
| const defaultAgent = defaults.agent || "all"; | ||
| const defaultMode = defaults.mode || "symlink"; | ||
| const defaultProjectDir = path.resolve(defaults.projectDir || process.cwd()); | ||
| const askMode = defaults.askMode !== false; | ||
| const agent = normalizeAgent(args.agent) || await chooseValue( | ||
| prompt, | ||
| "Agent to configure [all/claude/cursor/cline/roocode/copilot] (default: all): ", | ||
| `Agent to configure [all/claude/cursor/cline/roocode/copilot] (default: ${defaultAgent}): `, | ||
| AGENTS, | ||
| "all", | ||
| defaultAgent, | ||
| ); | ||
| const mode = normalizeMode(args.mode) || await chooseValue( | ||
| prompt, | ||
| "Install mode [copy/symlink] (default: symlink): ", | ||
| MODES, | ||
| "symlink", | ||
| ); | ||
| const projectDirInput = args.projectDir || await prompt.ask(`Project directory (default: ${process.cwd()}): `); | ||
| const projectDir = path.resolve(projectDirInput || process.cwd()); | ||
| const mode = askMode | ||
| ? (normalizeMode(args.mode) || await chooseValue( | ||
| prompt, | ||
| `Install mode [copy/symlink] (default: ${defaultMode}): `, | ||
| MODES, | ||
| defaultMode, | ||
| )) | ||
| : normalizeMode(args.mode || defaultMode); | ||
| let projectDir; | ||
| if (args.projectDir) { | ||
| projectDir = path.resolve(args.projectDir); | ||
| } else if (defaults.askProjectDir === false) { | ||
| projectDir = defaultProjectDir; | ||
| } else { | ||
| const projectDirInput = await prompt.ask(`Project directory (default: ${defaultProjectDir}): `); | ||
| projectDir = path.resolve(projectDirInput || defaultProjectDir); | ||
| } | ||
@@ -445,5 +666,5 @@ return { | ||
| function validateArgs(args) { | ||
| const agent = normalizeAgent(args.agent || "all"); | ||
| const mode = normalizeMode(args.mode || "symlink"); | ||
| function validateArgs(args, defaults = {}) { | ||
| const agent = normalizeAgent(args.agent || defaults.agent || "all"); | ||
| const mode = normalizeMode(args.mode || defaults.mode || "symlink"); | ||
@@ -461,16 +682,42 @@ if (!AGENTS.includes(agent)) { | ||
| mode, | ||
| projectDir: path.resolve(args.projectDir || process.cwd()), | ||
| projectDir: path.resolve(args.projectDir || defaults.projectDir || process.cwd()), | ||
| }; | ||
| } | ||
| async function runInstall(rawArgs) { | ||
| async function resolveSelection(rawArgs, options = {}) { | ||
| const packageRoot = getPackageRoot(); | ||
| const packageInstallType = options.packageInstallType || detectPackageInstallType( | ||
| packageRoot, | ||
| options.projectDir || rawArgs.projectDir || process.cwd(), | ||
| ); | ||
| const defaults = { | ||
| agent: "all", | ||
| mode: getDefaultMode(packageInstallType), | ||
| projectDir: options.projectDir || getDefaultProjectDir(), | ||
| askProjectDir: options.askProjectDir, | ||
| askMode: options.askMode, | ||
| }; | ||
| const selected = rawArgs.yes | ||
| ? validateArgs(rawArgs, defaults) | ||
| : await collectAnswers(rawArgs, defaults); | ||
| const installRoot = getInstallRoot(selected.projectDir, packageInstallType); | ||
| const installScope = packageInstallType === "global" ? "global" : "project"; | ||
| return { | ||
| packageInstallType, | ||
| selected, | ||
| installRoot, | ||
| installScope, | ||
| }; | ||
| } | ||
| async function runInstall(rawArgs, options = {}) { | ||
| const platform = detectPlatform(); | ||
| const packageRoot = getPackageRoot(); | ||
| const version = getVersion(); | ||
| const selected = rawArgs.yes ? validateArgs(rawArgs) : await collectAnswers(rawArgs); | ||
| const installRoot = getInstallRoot(selected.projectDir); | ||
| const packageInstallType = detectPackageInstallType(packageRoot, selected.projectDir); | ||
| const { packageInstallType, selected, installRoot, installScope } = await resolveSelection(rawArgs, options); | ||
| console.log(`[${PACKAGE_NAME}] installing version ${version}`); | ||
| console.log(`[${PACKAGE_NAME}] package install type: ${packageInstallType}`); | ||
| console.log(`[${PACKAGE_NAME}] skill install scope: ${installScope}`); | ||
| console.log(`[${PACKAGE_NAME}] os: ${platform}`); | ||
@@ -482,4 +729,5 @@ console.log(`[${PACKAGE_NAME}] package dir: ${packageRoot}`); | ||
| console.log(`[${PACKAGE_NAME}] mode: ${selected.mode}`); | ||
| console.log(`[${PACKAGE_NAME}] dry run: ${rawArgs.dryRun ? "yes" : "no"}`); | ||
| await ensureDir(installRoot); | ||
| await ensureDir(installRoot, rawArgs.dryRun); | ||
@@ -494,13 +742,14 @@ const installedSkills = []; | ||
| platform, | ||
| dryRun: rawArgs.dryRun, | ||
| }); | ||
| installedSkills.push(result); | ||
| console.log(`[${PACKAGE_NAME}] skill installed: ${result.skillName} -> ${result.destPath}`); | ||
| console.log(`[${PACKAGE_NAME}] skill ${rawArgs.dryRun ? "would install" : "installed"}: ${result.skillName} -> ${result.destPath}`); | ||
| } | ||
| const configured = await configureAgents(selected.agent, selected.projectDir, installRoot); | ||
| const configured = await configureAgents(selected.agent, selected.projectDir, installRoot, rawArgs.dryRun); | ||
| for (const item of configured) { | ||
| console.log(`[${PACKAGE_NAME}] ${item.agent} config updated: ${item.filePath}`); | ||
| console.log(`[${PACKAGE_NAME}] ${item.agent} config ${rawArgs.dryRun ? "would update" : "updated"}: ${item.filePath}`); | ||
| } | ||
| console.log(`[${PACKAGE_NAME}] install complete`); | ||
| console.log(`[${PACKAGE_NAME}] ${rawArgs.dryRun ? "install dry run complete" : "install complete"}`); | ||
@@ -513,2 +762,3 @@ return { | ||
| installRoot, | ||
| installScope, | ||
| installedSkills, | ||
@@ -519,2 +769,68 @@ configured, | ||
| async function runUninstall(rawArgs, options = {}) { | ||
| const version = getVersion(); | ||
| const { packageInstallType, selected, installRoot, installScope } = await resolveSelection( | ||
| rawArgs, | ||
| { ...options, askMode: false }, | ||
| ); | ||
| console.log(`[${PACKAGE_NAME}] uninstalling version ${version}`); | ||
| console.log(`[${PACKAGE_NAME}] package install type: ${packageInstallType}`); | ||
| console.log(`[${PACKAGE_NAME}] skill install scope: ${installScope}`); | ||
| console.log(`[${PACKAGE_NAME}] project dir: ${selected.projectDir}`); | ||
| console.log(`[${PACKAGE_NAME}] install dir: ${installRoot}`); | ||
| console.log(`[${PACKAGE_NAME}] agent: ${selected.agent}`); | ||
| console.log(`[${PACKAGE_NAME}] dry run: ${rawArgs.dryRun ? "yes" : "no"}`); | ||
| const configured = await unconfigureAgents(selected.agent, selected.projectDir, installRoot, rawArgs.dryRun); | ||
| for (const item of configured) { | ||
| console.log(`[${PACKAGE_NAME}] ${item.agent} config ${rawArgs.dryRun ? "would remove" : "removed"}: ${item.filePath}`); | ||
| } | ||
| for (const skillName of SKILLS) { | ||
| const skillPath = getSkillDestPath(installRoot, skillName); | ||
| await removePath(skillPath, rawArgs.dryRun); | ||
| console.log(`[${PACKAGE_NAME}] skill ${rawArgs.dryRun ? "would remove" : "removed"}: ${skillPath}`); | ||
| } | ||
| console.log(`[${PACKAGE_NAME}] ${rawArgs.dryRun ? "uninstall dry run complete" : "uninstall complete"}`); | ||
| return { | ||
| version, | ||
| packageInstallType, | ||
| projectDir: selected.projectDir, | ||
| installRoot, | ||
| installScope, | ||
| configured, | ||
| }; | ||
| } | ||
| async function runPostInstall(env = process.env) { | ||
| const packageRoot = getPackageRoot(); | ||
| const projectDir = getDefaultProjectDir(env); | ||
| const packageInstallType = detectPackageInstallType(packageRoot, projectDir); | ||
| if (packageInstallType === "source") { | ||
| console.log(`[${PACKAGE_NAME}] postinstall skipped in source checkout`); | ||
| return { skipped: true, reason: "source" }; | ||
| } | ||
| if (!isInteractiveInstall(env)) { | ||
| console.log(`[${PACKAGE_NAME}] postinstall skipped in non-interactive install`); | ||
| console.log(`[${PACKAGE_NAME}] run "npx developer-stack-skills install" to configure later`); | ||
| return { skipped: true, reason: "non-interactive" }; | ||
| } | ||
| console.log(`[${PACKAGE_NAME}] postinstall detected ${packageInstallType} package install`); | ||
| return runInstall( | ||
| { command: "install" }, | ||
| { | ||
| packageInstallType, | ||
| projectDir, | ||
| askProjectDir: packageInstallType === "global", | ||
| }, | ||
| ); | ||
| } | ||
| module.exports = { | ||
@@ -526,10 +842,19 @@ AGENTS, | ||
| configureAgents, | ||
| detectPackageInstallType, | ||
| detectPlatform, | ||
| getDefaultMode, | ||
| getDefaultProjectDir, | ||
| getInstallRoot, | ||
| isInteractiveInstall, | ||
| parseArgs, | ||
| printHelp, | ||
| printVersion, | ||
| removeManagedBlock, | ||
| removeSkillsSectionItems, | ||
| replaceManagedBlock, | ||
| runInstall, | ||
| runPostInstall, | ||
| runUninstall, | ||
| upsertSkillsSection, | ||
| validateArgs, | ||
| detectPackageInstallType, | ||
| }; |
+3
-1
| { | ||
| "name": "developer-stack-skills", | ||
| "version": "1.1.0", | ||
| "version": "1.2.0", | ||
| "description": "AI agent SKILL.md files plus installer CLI for Java/Spring, Python/FastAPI, React/Angular, Testing, and Project Conventions. Compatible with Claude, Cline, Roocode, Copilot, and Cursor.", | ||
@@ -33,2 +33,3 @@ "keywords": [ | ||
| "scripts": { | ||
| "postinstall": "node bin/postinstall.js", | ||
| "test": "node --test" | ||
@@ -46,2 +47,3 @@ }, | ||
| "README.md", | ||
| "RELEASE_NOTES.md", | ||
| "CHANGELOG.md" | ||
@@ -48,0 +50,0 @@ ], |
+61
-10
@@ -21,6 +21,16 @@ # developer-stack-skills | ||
| Version in this README: `1.1.0` | ||
| Version in this README: `1.2.0` | ||
| Run installer: | ||
| Interactive `npm install` now auto-runs configuration. | ||
| - `npm install developer-stack-skills` | ||
| Installs skills into `<project>/.ai-skills/developer-stack-skills` | ||
| Default mode prompt prefers `copy` | ||
| - `npm install -g developer-stack-skills` | ||
| Installs skills into `~/.ai-skills/developer-stack-skills` | ||
| Default mode prompt prefers `symlink` | ||
| Still asks which project directory to update for agent config files | ||
| Manual installer still available: | ||
| ```bash | ||
@@ -30,2 +40,20 @@ developer-stack-skills install | ||
| Remove installed skills and agent config: | ||
| ```bash | ||
| developer-stack-skills uninstall | ||
| ``` | ||
| Show version: | ||
| ```bash | ||
| developer-stack-skills version | ||
| ``` | ||
| Show help: | ||
| ```bash | ||
| developer-stack-skills help | ||
| ``` | ||
| Or run from local package without global install: | ||
@@ -42,5 +70,9 @@ | ||
| 3. Ask whether to `copy` files or create `symlink` | ||
| 4. Ask which project directory to install into | ||
| 5. Install all skill folders into: | ||
| 4. Ask which project directory to install into when needed | ||
| 5. Install all skill folders into project or global skill directory | ||
| 6. Update agent-specific config files in that project | ||
| 7. Log package version, package install type (`source`, `local`, or `global`), install scope, OS, source directory, install directory, and each generated config path | ||
| Project-level install dir: | ||
| ```text | ||
@@ -50,7 +82,10 @@ <project>/.ai-skills/developer-stack-skills/ | ||
| 6. Update agent-specific config files in that project | ||
| 7. Log package version, package install type (`local` or `global`), OS, source directory, install directory, and each generated config path | ||
| Global install dir: | ||
| Installer does not run automatically on `npm install`. Run `developer-stack-skills install` or `npx developer-stack-skills install` when you want to configure a project. | ||
| ```text | ||
| ~/.ai-skills/developer-stack-skills/ | ||
| ``` | ||
| `postinstall` skips auto-config in non-interactive environments and in source checkout of this repo. In those cases, run `developer-stack-skills install` or `npx developer-stack-skills install` manually. | ||
| Non-interactive install: | ||
@@ -60,2 +95,3 @@ | ||
| developer-stack-skills install --agent all --mode symlink --dir . --yes | ||
| developer-stack-skills uninstall --agent all --dir . --dry-run --yes | ||
| ``` | ||
@@ -66,9 +102,10 @@ | ||
| ```text | ||
| [developer-stack-skills] installing version 1.1.0 | ||
| [developer-stack-skills] installing version 1.2.0 | ||
| [developer-stack-skills] package install type: global | ||
| [developer-stack-skills] skill install scope: global | ||
| [developer-stack-skills] os: windows | ||
| [developer-stack-skills] package dir: C:\Users\<you>\AppData\Roaming\npm\node_modules\developer-stack-skills | ||
| [developer-stack-skills] project dir: D:\Projects\my-app | ||
| [developer-stack-skills] install dir: D:\Projects\my-app\.ai-skills\developer-stack-skills | ||
| [developer-stack-skills] skill installed: java-spring -> D:\Projects\my-app\.ai-skills\developer-stack-skills\java-spring | ||
| [developer-stack-skills] install dir: C:\Users\<you>\.ai-skills\developer-stack-skills | ||
| [developer-stack-skills] skill installed: java-spring -> C:\Users\<you>\.ai-skills\developer-stack-skills\java-spring | ||
| [developer-stack-skills] cline config updated: D:\Projects\my-app\.clinerules | ||
@@ -83,4 +120,12 @@ [developer-stack-skills] install complete | ||
| - `--dir <project-directory>` | ||
| - `--dry-run` | ||
| - `--yes` | ||
| Commands: | ||
| - `install` | ||
| - `uninstall` | ||
| - `version` | ||
| - `help` | ||
| --- | ||
@@ -96,2 +141,8 @@ | ||
| Or for global package installs: | ||
| ```text | ||
| ~/.ai-skills/developer-stack-skills/ | ||
| ``` | ||
| Agent configs get created or updated here: | ||
@@ -98,0 +149,0 @@ |
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
98944
17.96%21
10.53%740
69.72%181
39.23%0
-100%1
Infinity%6
100%