Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/skillkit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { Cli, Builtins } from 'clipanion';
import { setVersion, setAgentCount } from '@skillkit/cli';
import { getAdapterCount } from '@skillkit/agents';
import {
InstallCommand,
SyncCommand,
Expand Down Expand Up @@ -130,6 +132,9 @@ const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version || '1.2.0';

setVersion(version);
setAgentCount(getAdapterCount());

const cli = new Cli({
binaryLabel: 'skillkit',
binaryName: 'skillkit',
Expand Down
4 changes: 4 additions & 0 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ export function getAllAdapters(): AgentAdapterWithType[] {
}));
}

export function getAdapterCount(): number {
return Object.keys(adapters).length;
}

export async function detectAgent(): Promise<AgentType> {
const checkOrder: AgentType[] = [
'claude-code',
Expand Down
10 changes: 4 additions & 6 deletions packages/cli/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
isCancel,
spinner,
quickAgentSelect,
skillMultiselect,
quickSkillSelect,
selectInstallMethod,
confirm,
outro,
Expand Down Expand Up @@ -273,22 +273,20 @@ export class InstallCommand extends Command {
} else if (this.all || this.yes) {
skillsToInstall = discoveredSkills;
} else if (isInteractive && discoveredSkills.length > 1) {
// Interactive skill selection
step(`Source: ${colors.cyan(this.source)}`);

const skillResult = await skillMultiselect({
message: "Select skills to install",
const skillResult = await quickSkillSelect({
skills: discoveredSkills.map((s) => ({ name: s.name })),
initialValues: discoveredSkills.map((s) => s.name),
});

if (isCancel(skillResult)) {
cancel("Installation cancelled");
return 0;
}

const selected = (skillResult as { skills: string[] }).skills;
skillsToInstall = discoveredSkills.filter((s) =>
(skillResult as string[]).includes(s.name),
selected.includes(s.name),
);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export * from './commands/index.js';
// Export helper functions
export * from './helpers.js';

// Export onboarding utilities
export { setVersion, setAgentCount } from './onboarding/index.js';

// Re-export commonly used types from core
export type { AgentType, Skill, SkillMetadata, SkillkitConfig } from '@skillkit/core';
35 changes: 20 additions & 15 deletions packages/cli/src/onboarding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export {
select,
agentMultiselect,
quickAgentSelect,
quickSkillSelect,
skillMultiselect,
groupMultiselect,
stepTrail,
Expand Down Expand Up @@ -87,8 +88,8 @@ import {
getLastAgents,
} from './preferences.js';

let VERSION = '1.7.6';
let AGENT_COUNT = 44;
let VERSION = 'dev';
let AGENT_COUNT = 0;

export function setVersion(version: string): void {
VERSION = version;
Expand Down Expand Up @@ -161,18 +162,22 @@ export async function runInstallFlow(options: InstallFlowOptions): Promise<Insta
}

prompts.step(`Source: ${colors.cyan(source)}`);
prompts.step(`Found ${discoveredSkills.length} skill${discoveredSkills.length !== 1 ? 's' : ''}`);

const skillNames = discoveredSkills.map(s => s.name);
const skillResult = await prompts.skillMultiselect({
message: 'Select skills to install',
skills: discoveredSkills.map(s => ({ name: s.name })),
initialValues: skillNames,
});
let selectedSkills: string[];

if (prompts.isCancel(skillResult)) {
prompts.cancel('Installation cancelled');
return { selectedSkills: [], selectedAgents: [], installMethod: 'symlink', cancelled: true };
if (discoveredSkills.length > 1) {
const skillResult = await prompts.quickSkillSelect({
skills: discoveredSkills.map(s => ({ name: s.name })),
});

if (prompts.isCancel(skillResult)) {
prompts.cancel('Installation cancelled');
return { selectedSkills: [], selectedAgents: [], installMethod: 'symlink', cancelled: true };
}

selectedSkills = (skillResult as { skills: string[] }).skills;
} else {
selectedSkills = discoveredSkills.map(s => s.name);
}

if (detectedAgents.length > 1) {
Expand All @@ -190,7 +195,7 @@ export async function runInstallFlow(options: InstallFlowOptions): Promise<Insta

if (prompts.isCancel(agentResult)) {
prompts.cancel('Installation cancelled');
return { selectedSkills: skillResult as string[], selectedAgents: [], installMethod: 'symlink', cancelled: true };
return { selectedSkills, selectedAgents: [], installMethod: 'symlink', cancelled: true };
}

saveLastAgents(agentResult as string[]);
Expand All @@ -202,15 +207,15 @@ export async function runInstallFlow(options: InstallFlowOptions): Promise<Insta
if (prompts.isCancel(methodResult)) {
prompts.cancel('Installation cancelled');
return {
selectedSkills: skillResult as string[],
selectedSkills,
selectedAgents: agentResult as string[],
installMethod: 'symlink',
cancelled: true,
};
}

return {
selectedSkills: skillResult as string[],
selectedSkills,
selectedAgents: agentResult as string[],
installMethod: methodResult as 'symlink' | 'copy',
cancelled: false,
Expand Down
37 changes: 37 additions & 0 deletions packages/cli/src/onboarding/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,43 @@ export async function skillMultiselect(options: {
});
}

export async function quickSkillSelect(options: {
message?: string;
skills: SkillOption[];
}): Promise<{ skills: string[]; method: 'all' | 'select' } | symbol> {
const { skills } = options;

const result = await clack.select({
message: options.message || `Found ${skills.length} skills — how would you like to install?`,
options: [
{ value: 'all' as const, label: 'Install all skills', hint: `${skills.length} skills` },
{ value: 'select' as const, label: 'Select specific skills', hint: 'Choose manually' },
],
});

if (clack.isCancel(result)) {
return result;
}

const method = result as 'all' | 'select';

if (method === 'all') {
return { skills: skills.map(s => s.name), method };
}

const selected = await skillMultiselect({
message: 'Select skills to install',
skills,
initialValues: [],
});

if (clack.isCancel(selected)) {
return selected;
}

return { skills: selected as string[], method };
Comment on lines +240 to +250
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILES=$(fd -t f 'prompts\.ts|install\.ts|index\.ts' packages/cli/src)

echo "== quickSkillSelect call sites =="
rg -n --type=ts '\bquickSkillSelect\s*\(' $FILES

echo
echo "== Empty-selection guards / handling near relevant code =="
rg -n --type=ts -C4 '\bquickSkillSelect\b|skills\.length|No skills selected|no-op|cancel' $FILES

echo
echo "== skillMultiselect required/default behavior =="
rg -n --type=ts -C3 '\bskillMultiselect\b|required:\s*options\.required\s*\?\?' packages/cli/src/onboarding/prompts.ts

Repository: rohitg00/skillkit

Length of output: 14517


🏁 Script executed:

#!/bin/bash

echo "=== runInstallFlow: downstream handling of selectedSkills ==="
sed -n '157,223p' packages/cli/src/onboarding/index.ts | cat -n

echo
echo "=== install.ts: downstream handling of selected skills ==="
sed -n '278,360p' packages/cli/src/commands/install.ts | cat -n

Repository: rohitg00/skillkit

Length of output: 5692


🏁 Script executed:

#!/bin/bash

echo "=== Callers of runInstallFlow ==="
rg -n 'runInstallFlow' --type=ts packages/cli/src -A 5

echo
echo "=== Check for selectedSkills validation in callers ==="
rg -n 'runInstallFlow|selectedSkills\.length|selectedSkills.*===' --type=ts packages/cli/src -B 2 -A 2

Repository: rohitg00/skillkit

Length of output: 997


🏁 Script executed:

#!/bin/bash

echo "=== All references to runInstallFlow (excluding definition) ==="
rg 'runInstallFlow' --type=ts packages/cli/src | grep -v 'export async function runInstallFlow'

echo
echo "=== Context around runInstallFlow calls in main execution ==="
fd -t f -name "*.ts" packages/cli/src | xargs rg -l 'runInstallFlow' | head -5 | while read f; do
  echo "=== File: $f ==="
  rg -n -B 5 -A 10 'runInstallFlow\(' "$f" 2>/dev/null || true
done

Repository: rohitg00/skillkit

Length of output: 940


🏁 Script executed:

#!/bin/bash

echo "=== Export check in onboarding/index.ts ==="
rg 'export.*runInstallFlow|runInstallFlow' packages/cli/src/onboarding/index.ts

echo
echo "=== Search for imports of runInstallFlow ==="
rg 'import.*runInstallFlow|from.*onboarding' --type=ts packages/cli/src

echo
echo "=== Check if runInstallFlow is exported from main package index ==="
if [ -f packages/cli/src/index.ts ]; then
  cat packages/cli/src/index.ts | head -50
fi

echo
echo "=== Look for package.json exports ==="
cat packages/cli/package.json | grep -A 20 '"exports"' || echo "No exports field"

Repository: rohitg00/skillkit

Length of output: 2724


🏁 Script executed:

#!/bin/bash

echo "=== Check what's actually exported from onboarding/index.ts ==="
sed -n '1,60p' packages/cli/src/onboarding/index.ts | grep -E '^export|^import'

echo
echo "=== Full export list from onboarding/index.ts ==="
rg '^export' packages/cli/src/onboarding/index.ts | head -30

echo
echo "=== Is runInstallFlow actually used in any test or external package? ==="
find . -name "*.test.ts" -o -name "*.spec.ts" | head -5 | while read f; do
  grep -l "runInstallFlow" "$f" 2>/dev/null || true
done

echo
echo "=== Look in other packages for runInstallFlow usage ==="
find packages -name "*.ts" ! -path "*/cli/src/*" -exec grep -l "runInstallFlow" {} \; 2>/dev/null | head -5 || echo "No usage found in other packages"

Repository: rohitg00/skillkit

Length of output: 995


🏁 Script executed:

#!/bin/bash

echo "=== Exact code at lines 240-250 in prompts.ts ==="
sed -n '240,250p' packages/cli/src/onboarding/prompts.ts | cat -n

echo
echo "=== Full quickSkillSelect function definition ==="
sed -n '216,251p' packages/cli/src/onboarding/prompts.ts | cat -n

echo
echo "=== Trace: what happens when empty skills array is returned ==="
echo "Call site 1: onboarding/index.ts"
sed -n '168,184p' packages/cli/src/onboarding/index.ts | cat -n

echo
echo "Call site 2: commands/install.ts"
sed -n '275,295p' packages/cli/src/commands/install.ts | cat -n

Repository: rohitg00/skillkit

Length of output: 3349


Empty skills selection requires consistent handling across call sites.

skillMultiselect allows empty selection (required: false), so quickSkillSelect can return { skills: [] } when users submit no selections. This is handled explicitly in commands/install.ts (line 294 guard), but onboarding/index.ts in runInstallFlow() returns the empty array without validation. While runInstallFlow is exported but unused in the current codebase, add a guard for consistency if this is intentional, or validate at the call site to prevent downstream issues:

if (skillResult.skills.length === 0) {
  prompts.warn('No skills selected');
  // re-prompt or cancel
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/onboarding/prompts.ts` around lines 240 - 250,
quickSkillSelect/skillMultiselect allows an empty selection but runInstallFlow
currently returns an empty skills array without validation; update
runInstallFlow (the exported function in onboarding/index.ts) to detect when
skillResult.skills.length === 0 and handle it consistently with
commands/install.ts (e.g., call prompts.warn('No skills selected') and either
re-prompt or return a cancel result), or alternatively validate at every call
site that consumes runInstallFlow so downstream code never receives an empty
skills array.

}

export async function groupMultiselect<T extends string>(options: {
message: string;
options: Record<string, Array<{ value: T; label: string; hint?: string }>>;
Expand Down
Loading