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
31 changes: 19 additions & 12 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,29 +387,36 @@ dnx okf -- spec -v 0.2 -o SPEC.md
## `skill`

Install or remove the bundled [agent skill](skills/okf/SKILL.md) that teaches
coding agents how to run `dnx okf`. Same path rules as
[go#’s skill command](https://github.com/devlooped/go#agent-skill):
coding agents how to run `dnx okf`. Writes to `.agents/skills/okf/SKILL.md`
under the chosen base directory.

```bash
# Install to ~/.agents/skills/okf/SKILL.md (prompts for confirmation)
# Interactive: choose Local (.) vs Global (~)
dnx okf -- skill

# Install for the current project under .agents/skills/okf/SKILL.md
# Install under the current directory
dnx okf -- skill .

# Skip the confirmation prompt
dnx okf -- skill -y
dnx okf -- skill . --yes
# Install under the user home directory
dnx okf -- skill -g
dnx okf -- skill --global

# Remove a previously installed skill (same path rules as install)
dnx okf -- skill remove
# Skip the confirmation prompt (directory or --global required)
dnx okf -- skill . -y
dnx okf -- skill -g --yes

# Remove a previously installed skill
dnx okf -- skill remove # only one copy → remove it; both → pick
dnx okf -- skill remove .
dnx okf -- skill remove -g
dnx okf -- skill remove -y
```

With no directory, the skill is written under the user home directory. Pass a
base directory (commonly `.`) to install under that location instead. Either
form overwrites an existing install.
With no directory and no `--global`, `skill` prompts for **Local** (same
destination as `.`) or **Global** (`~\.agents\skills\okf\SKILL.md`, including
on Windows). `skill remove` with no destination removes the only installed
copy; if both Local and Global exist, it prompts. Pass a base directory or
`-g`/`--global` to skip the picker. Either form overwrites an existing install.

---

Expand Down
2 changes: 1 addition & 1 deletion skills/okf/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Do not fetch the spec over the network. `schema` is the graph JSON Schema for
| `schema [-v ver] [-o file]` | Graph JSON Schema (`okf.json` shape) to stdout or file |
| `spec [-v ver] [-o file]` | OKF spec markdown to stdout or file |
| `view [path]` | HTML reader + full body+nav graph |
| `skill [dir]` | Install this skill (`skill remove` to uninstall) |
| `skill [dir] [-g]` | Install this skill (`skill remove` to uninstall) |

`-v` / `--version` selects a bundled format version (`latest` by default).
Pass an explicit version when a bundle declares `okf_version`. Unknown
Expand Down
156 changes: 156 additions & 0 deletions src/Tests/SkillCommandsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,156 @@ public void ResolveSkillPath_uses_directory_when_provided()
SkillCommands.ResolveSkillPath("."));
}

[Fact]
public void FormatScopePath_uses_dot_for_local_and_tilde_for_global()
{
var local = SkillCommands.FormatScopePath(false);
var global = SkillCommands.FormatScopePath(true);
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

Assert.Equal(Path.Combine(".", ".agents", "skills", "okf", "SKILL.md"), local);
Assert.Equal(Path.Combine("~", ".agents", "skills", "okf", "SKILL.md"), global);
Assert.StartsWith("." + Path.DirectorySeparatorChar, local);
Assert.StartsWith("~" + Path.DirectorySeparatorChar, global);
Assert.DoesNotContain(home, global);
}

[Fact]
public void ResolveInstallDestination_prompts_when_unspecified()
{
var result = SkillCommands.ResolveInstallDestination(null, global: false);
Assert.Equal(SkillCommands.DestinationKind.Prompt, result.Kind);
Assert.Null(result.Path);
}

[Fact]
public void ResolveInstallDestination_global_uses_home()
{
var result = SkillCommands.ResolveInstallDestination(null, global: true);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(SkillCommands.ResolveSkillPath(null), result.Path);
Assert.True(result.Confirm);
}

[Fact]
public void ResolveInstallDestination_directory_uses_that_base()
{
var root = CreateTempDir();
var result = SkillCommands.ResolveInstallDestination(root, global: false);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(SkillCommands.ResolveSkillPath(root), result.Path);
Assert.True(result.Confirm);
}

[Fact]
public void ResolveInstallDestination_rejects_directory_and_global()
{
var result = SkillCommands.ResolveInstallDestination(".", global: true);
Assert.Equal(SkillCommands.DestinationKind.Error, result.Kind);
Assert.Contains("--global", result.Error);
}

[Fact]
public void ResolveRemoveDestination_prompts_when_both_exist()
{
var local = SkillCommands.ResolveSkillPath(CreateTempDir());
var global = SkillCommands.ResolveSkillPath(CreateTempDir());
WriteSkill(local);
WriteSkill(global);

var result = SkillCommands.ResolveRemoveDestination(null, global: false, local, global);
Assert.Equal(SkillCommands.DestinationKind.Prompt, result.Kind);
}

[Fact]
public void ResolveRemoveDestination_removes_only_local_without_confirm()
{
var local = SkillCommands.ResolveSkillPath(CreateTempDir());
var global = SkillCommands.ResolveSkillPath(CreateTempDir());
WriteSkill(local);

var result = SkillCommands.ResolveRemoveDestination(null, global: false, local, global);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(local, result.Path);
Assert.False(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_removes_only_global_without_confirm()
{
var local = SkillCommands.ResolveSkillPath(CreateTempDir());
var global = SkillCommands.ResolveSkillPath(CreateTempDir());
WriteSkill(global);

var result = SkillCommands.ResolveRemoveDestination(null, global: false, local, global);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(global, result.Path);
Assert.False(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_neither_exists_does_not_prompt()
{
var local = SkillCommands.ResolveSkillPath(CreateTempDir());
var global = SkillCommands.ResolveSkillPath(CreateTempDir());

var result = SkillCommands.ResolveRemoveDestination(null, global: false, local, global);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.False(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_same_location_does_not_prompt()
{
var dest = SkillCommands.ResolveSkillPath(CreateTempDir());
WriteSkill(dest);

var result = SkillCommands.ResolveRemoveDestination(null, global: false, dest, dest);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(dest, result.Path);
Assert.False(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_explicit_directory_confirms()
{
var root = CreateTempDir();
var result = SkillCommands.ResolveRemoveDestination(root, global: false);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(SkillCommands.ResolveSkillPath(root), result.Path);
Assert.True(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_global_flag_confirms()
{
var result = SkillCommands.ResolveRemoveDestination(null, global: true);
Assert.Equal(SkillCommands.DestinationKind.Target, result.Kind);
Assert.Equal(SkillCommands.ResolveSkillPath(null), result.Path);
Assert.True(result.Confirm);
}

[Fact]
public void ResolveRemoveDestination_rejects_directory_and_global()
{
var result = SkillCommands.ResolveRemoveDestination(".", global: true);
Assert.Equal(SkillCommands.DestinationKind.Error, result.Kind);
Assert.Contains("--global", result.Error);
}

[Fact]
public void RenderScopeLine_marks_selected_local_path()
{
var path = SkillCommands.FormatScopePath(false);
var selected = SkillCommands.RenderScopeLine("Local", path, selected: true);
var idle = SkillCommands.RenderScopeLine("Global", SkillCommands.FormatScopePath(true), selected: false);

Assert.Contains("[green]●[/] Local", selected);
Assert.Contains(path, selected);
Assert.Contains("[grey]○ Global", idle);
Assert.DoesNotContain("[green]", idle);
}

[Fact]
public void Bundled_skill_matches_repo_skill_file()
{
Expand Down Expand Up @@ -67,6 +217,12 @@ public void Uninstall_succeeds_when_not_installed()
Assert.Equal(0, SkillCommands.Uninstall(dest));
}

static void WriteSkill(string dest)
{
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
File.WriteAllText(dest, "skill");
}

static string CreateTempDir()
{
var dir = Path.Combine(Path.GetTempPath(), "okf-tests-" + Guid.NewGuid().ToString("N"));
Expand Down
Loading
Loading