@kurokeita/add-skill 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/dist/agents/backend-specialist.md +277 -0
- package/dist/agents/code-archaeologist.md +114 -0
- package/dist/agents/database-architect.md +238 -0
- package/dist/agents/debugger.md +231 -0
- package/dist/agents/devops-engineer.md +242 -0
- package/dist/agents/documentation-writer.md +104 -0
- package/dist/agents/explorer-agent.md +79 -0
- package/dist/agents/frontend-specialist.md +594 -0
- package/dist/agents/game-developer.md +162 -0
- package/dist/agents/mobile-developer.md +383 -0
- package/dist/agents/orchestrator.md +435 -0
- package/dist/agents/penetration-tester.md +188 -0
- package/dist/agents/performance-optimizer.md +191 -0
- package/dist/agents/product-manager.md +122 -0
- package/dist/agents/product-owner.md +106 -0
- package/dist/agents/project-planner.md +408 -0
- package/dist/agents/qa-automation-engineer.md +109 -0
- package/dist/agents/security-auditor.md +171 -0
- package/dist/agents/test-engineer.md +158 -0
- package/dist/bin/cli.js +29 -0
- package/dist/src/commands/add.js +115 -77
- package/dist/src/commands/import.js +73 -23
- package/dist/src/commands/list.js +84 -23
- package/dist/src/commands/remove.js +118 -0
- package/dist/src/utils/github.js +22 -11
- package/dist/src/utils/paths.js +45 -0
- package/dist/workflows/brainstorm.md +113 -0
- package/dist/workflows/create.md +60 -0
- package/dist/workflows/debug.md +107 -0
- package/dist/workflows/deploy.md +178 -0
- package/dist/workflows/enhance.md +64 -0
- package/dist/workflows/orchestrate.md +246 -0
- package/dist/workflows/plan.md +90 -0
- package/dist/workflows/preview.md +83 -0
- package/dist/workflows/status.md +87 -0
- package/dist/workflows/test.md +144 -0
- package/dist/workflows/ui-ux-pro-max.md +315 -0
- package/package.json +4 -4
- package/dist/bin/skills.js +0 -17
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-engineer
|
|
3
|
+
description: Expert in testing, TDD, and test automation. Use for writing tests, improving coverage, debugging test failures. Triggers on test, spec, coverage, jest, pytest, playwright, e2e, unit test.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Edit, Write
|
|
5
|
+
model: inherit
|
|
6
|
+
skills: clean-code, testing-patterns, tdd-workflow, webapp-testing, code-review-checklist, lint-and-validate
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Test Engineer
|
|
10
|
+
|
|
11
|
+
Expert in test automation, TDD, and comprehensive testing strategies.
|
|
12
|
+
|
|
13
|
+
## Core Philosophy
|
|
14
|
+
|
|
15
|
+
> "Find what the developer forgot. Test behavior, not implementation."
|
|
16
|
+
|
|
17
|
+
## Your Mindset
|
|
18
|
+
|
|
19
|
+
- **Proactive**: Discover untested paths
|
|
20
|
+
- **Systematic**: Follow testing pyramid
|
|
21
|
+
- **Behavior-focused**: Test what matters to users
|
|
22
|
+
- **Quality-driven**: Coverage is a guide, not a goal
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Testing Pyramid
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/\ E2E (Few)
|
|
30
|
+
/ \ Critical user flows
|
|
31
|
+
/----\
|
|
32
|
+
/ \ Integration (Some)
|
|
33
|
+
/--------\ API, DB, services
|
|
34
|
+
/ \
|
|
35
|
+
/------------\ Unit (Many)
|
|
36
|
+
Functions, logic
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Framework Selection
|
|
42
|
+
|
|
43
|
+
| Language | Unit | Integration | E2E |
|
|
44
|
+
|----------|------|-------------|-----|
|
|
45
|
+
| TypeScript | Vitest, Jest | Supertest | Playwright |
|
|
46
|
+
| Python | Pytest | Pytest | Playwright |
|
|
47
|
+
| React | Testing Library | MSW | Playwright |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## TDD Workflow
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
🔴 RED → Write failing test
|
|
55
|
+
🟢 GREEN → Minimal code to pass
|
|
56
|
+
🔵 REFACTOR → Improve code quality
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Test Type Selection
|
|
62
|
+
|
|
63
|
+
| Scenario | Test Type |
|
|
64
|
+
|----------|-----------|
|
|
65
|
+
| Business logic | Unit |
|
|
66
|
+
| API endpoints | Integration |
|
|
67
|
+
| User flows | E2E |
|
|
68
|
+
| Components | Component/Unit |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## AAA Pattern
|
|
73
|
+
|
|
74
|
+
| Step | Purpose |
|
|
75
|
+
|------|---------|
|
|
76
|
+
| **Arrange** | Set up test data |
|
|
77
|
+
| **Act** | Execute code |
|
|
78
|
+
| **Assert** | Verify outcome |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Coverage Strategy
|
|
83
|
+
|
|
84
|
+
| Area | Target |
|
|
85
|
+
|------|--------|
|
|
86
|
+
| Critical paths | 100% |
|
|
87
|
+
| Business logic | 80%+ |
|
|
88
|
+
| Utilities | 70%+ |
|
|
89
|
+
| UI layout | As needed |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Deep Audit Approach
|
|
94
|
+
|
|
95
|
+
### Discovery
|
|
96
|
+
|
|
97
|
+
| Target | Find |
|
|
98
|
+
|--------|------|
|
|
99
|
+
| Routes | Scan app directories |
|
|
100
|
+
| APIs | Grep HTTP methods |
|
|
101
|
+
| Components | Find UI files |
|
|
102
|
+
|
|
103
|
+
### Systematic Testing
|
|
104
|
+
|
|
105
|
+
1. Map all endpoints
|
|
106
|
+
2. Verify responses
|
|
107
|
+
3. Cover critical paths
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Mocking Principles
|
|
112
|
+
|
|
113
|
+
| Mock | Don't Mock |
|
|
114
|
+
|------|------------|
|
|
115
|
+
| External APIs | Code under test |
|
|
116
|
+
| Database (unit) | Simple deps |
|
|
117
|
+
| Network | Pure functions |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Review Checklist
|
|
122
|
+
|
|
123
|
+
- [ ] Coverage 80%+ on critical paths
|
|
124
|
+
- [ ] AAA pattern followed
|
|
125
|
+
- [ ] Tests are isolated
|
|
126
|
+
- [ ] Descriptive naming
|
|
127
|
+
- [ ] Edge cases covered
|
|
128
|
+
- [ ] External deps mocked
|
|
129
|
+
- [ ] Cleanup after tests
|
|
130
|
+
- [ ] Fast unit tests (<100ms)
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Anti-Patterns
|
|
135
|
+
|
|
136
|
+
| ❌ Don't | ✅ Do |
|
|
137
|
+
|----------|-------|
|
|
138
|
+
| Test implementation | Test behavior |
|
|
139
|
+
| Multiple asserts | One per test |
|
|
140
|
+
| Dependent tests | Independent |
|
|
141
|
+
| Ignore flaky | Fix root cause |
|
|
142
|
+
| Skip cleanup | Always reset |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## When You Should Be Used
|
|
147
|
+
|
|
148
|
+
- Writing unit tests
|
|
149
|
+
- TDD implementation
|
|
150
|
+
- E2E test creation
|
|
151
|
+
- Improving coverage
|
|
152
|
+
- Debugging test failures
|
|
153
|
+
- Test infrastructure setup
|
|
154
|
+
- API integration tests
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
> **Remember:** Good tests are documentation. They explain what the code should do.
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { add } from "../src/commands/add.js";
|
|
4
|
+
import { importItem } from "../src/commands/import.js";
|
|
5
|
+
import { list } from "../src/commands/list.js";
|
|
6
|
+
import { remove } from "../src/commands/remove.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("ai-agent")
|
|
10
|
+
.description("CLI to manage AI agents, skills, and workflows")
|
|
11
|
+
.version("1.4.0");
|
|
12
|
+
program
|
|
13
|
+
.command("list [type]")
|
|
14
|
+
.description("List available items (skills, agents, workflows)")
|
|
15
|
+
.option("-l, --local", "List installed items locally")
|
|
16
|
+
.action(list);
|
|
17
|
+
program
|
|
18
|
+
.command("add <type> [url]")
|
|
19
|
+
.description("Add an item (skill, agent, workflow) to platforms (Interactive or from GitHub URL)")
|
|
20
|
+
.action(add);
|
|
21
|
+
program
|
|
22
|
+
.command("import <type> <url>")
|
|
23
|
+
.description("Import an item from GitHub to the repo")
|
|
24
|
+
.action(importItem);
|
|
25
|
+
program
|
|
26
|
+
.command("remove <type>")
|
|
27
|
+
.description("Remove locally installed items (skills, agents, workflows)")
|
|
28
|
+
.action(remove);
|
|
29
|
+
program.parse();
|
package/dist/src/commands/add.js
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { cancel, confirm, intro, isCancel, multiselect, note, outro, spinner, } from "@clack/prompts";
|
|
5
4
|
import fs from "fs-extra";
|
|
6
5
|
import pc from "picocolors";
|
|
7
6
|
import { fetchSkillFromGitHub } from "../utils/github.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
label: "Antigravity",
|
|
21
|
-
value: "antigravity",
|
|
22
|
-
hint: "~/.gemini/antigravity/global_skills",
|
|
23
|
-
},
|
|
24
|
-
{ label: "Gemini CLI", value: "gemini", hint: "~/.gemini/skills" },
|
|
25
|
-
{ label: "GitHub Copilot", value: "copilot", hint: "~/.copilot/skills" },
|
|
26
|
-
{
|
|
27
|
-
label: "Windsurf",
|
|
28
|
-
value: "windsurf",
|
|
29
|
-
hint: "~/.codeium/windsurf/skills",
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
async function installSkill(skillName, platform, overwrite, sourcePath = path.join(SKILLS_DIR, skillName)) {
|
|
33
|
-
const targetBaseDir = PLATFORM_PATHS[platform];
|
|
34
|
-
const targetPath = path.join(targetBaseDir, skillName);
|
|
7
|
+
import { getTargetPaths, PLATFORM_LABELS, TYPE_DIRS, } from "../utils/paths.js";
|
|
8
|
+
function getPlatformOptions(type) {
|
|
9
|
+
const paths = getTargetPaths(type);
|
|
10
|
+
return Object.entries(paths).map(([platform, pathStr]) => ({
|
|
11
|
+
label: PLATFORM_LABELS[platform],
|
|
12
|
+
value: platform,
|
|
13
|
+
hint: pathStr.replace(os.homedir(), "~"),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
async function installItem(itemName, overwrite, sourcePath, targetBaseDir) {
|
|
17
|
+
const targetPath = path.join(targetBaseDir, itemName);
|
|
35
18
|
if (!(await fs.pathExists(sourcePath))) {
|
|
36
|
-
throw new Error(`
|
|
19
|
+
throw new Error(`Item '${itemName}' not found at ${sourcePath}`);
|
|
37
20
|
}
|
|
38
21
|
if (!overwrite && (await fs.pathExists(targetPath))) {
|
|
39
22
|
return false;
|
|
@@ -42,58 +25,76 @@ async function installSkill(skillName, platform, overwrite, sourcePath = path.jo
|
|
|
42
25
|
await fs.copy(sourcePath, targetPath, { overwrite });
|
|
43
26
|
return true;
|
|
44
27
|
}
|
|
45
|
-
export async function
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
export async function add(type, url) {
|
|
29
|
+
intro(pc.bgCyan(pc.black(` AI Manager : Add ${type} `)));
|
|
30
|
+
// Normalize type
|
|
31
|
+
const normalizedType = type.toLowerCase().endsWith("s")
|
|
32
|
+
? type.slice(0, -1)
|
|
33
|
+
: type;
|
|
34
|
+
if (!TYPE_DIRS[normalizedType]) {
|
|
35
|
+
cancel(`Unknown type: ${type}. Supported: skill, agent, workflow`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
48
38
|
let tempDir = null;
|
|
49
|
-
let
|
|
39
|
+
let selectedItems = [];
|
|
50
40
|
try {
|
|
51
41
|
// 1. Determine Source (Local vs GitHub)
|
|
52
42
|
if (url) {
|
|
53
43
|
const s = spinner();
|
|
54
|
-
s.start(
|
|
44
|
+
s.start(`Fetching ${normalizedType} from GitHub...`);
|
|
55
45
|
try {
|
|
56
46
|
const result = await fetchSkillFromGitHub(url);
|
|
57
47
|
tempDir = result.tempDir;
|
|
58
|
-
|
|
59
|
-
s.stop(pc.green(`Fetched
|
|
48
|
+
selectedItems = [result.skillName];
|
|
49
|
+
s.stop(pc.green(`Fetched ${normalizedType}: ${result.skillName}`));
|
|
60
50
|
}
|
|
61
51
|
catch (e) {
|
|
62
|
-
s.stop(pc.red("Failed to fetch
|
|
52
|
+
s.stop(pc.red("Failed to fetch"));
|
|
63
53
|
throw e;
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
56
|
else {
|
|
67
57
|
// Local Selection Logic
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
const sourceDir = TYPE_DIRS[normalizedType];
|
|
59
|
+
if (!(await fs.pathExists(sourceDir))) {
|
|
60
|
+
cancel(`${normalizedType} directory not found!`);
|
|
70
61
|
process.exit(1);
|
|
71
62
|
}
|
|
72
|
-
const entries = await fs.readdir(
|
|
73
|
-
const
|
|
74
|
-
.filter((entry) => entry.isDirectory()
|
|
63
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
64
|
+
const availableItems = entries
|
|
65
|
+
.filter((entry) => entry.isDirectory() ||
|
|
66
|
+
((normalizedType === "workflow" || normalizedType === "agent") &&
|
|
67
|
+
entry.isFile() &&
|
|
68
|
+
entry.name.endsWith(".md")))
|
|
75
69
|
.map((entry) => ({ label: entry.name, value: entry.name }))
|
|
76
70
|
.sort((a, b) => a.label.localeCompare(b.label));
|
|
77
|
-
if (
|
|
78
|
-
cancel(
|
|
71
|
+
if (availableItems.length === 0) {
|
|
72
|
+
cancel(`No ${normalizedType}s found in directory.`);
|
|
79
73
|
process.exit(0);
|
|
80
74
|
}
|
|
81
|
-
// Select
|
|
82
|
-
const
|
|
83
|
-
message:
|
|
84
|
-
options:
|
|
75
|
+
// Select Items
|
|
76
|
+
const items = await multiselect({
|
|
77
|
+
message: `Select ${normalizedType}s to install:`,
|
|
78
|
+
options: availableItems,
|
|
85
79
|
required: true,
|
|
86
80
|
});
|
|
87
|
-
if (isCancel(
|
|
81
|
+
if (isCancel(items)) {
|
|
88
82
|
cancel("Operation cancelled.");
|
|
89
83
|
process.exit(0);
|
|
90
84
|
}
|
|
91
|
-
|
|
85
|
+
selectedItems = items;
|
|
92
86
|
}
|
|
93
87
|
// 2. Select Platforms
|
|
88
|
+
// Filter platform options based on support for the type
|
|
89
|
+
const supportedPaths = getTargetPaths(normalizedType);
|
|
90
|
+
const currentPlatformOptions = getPlatformOptions(normalizedType);
|
|
91
|
+
if (currentPlatformOptions.length === 0) {
|
|
92
|
+
cancel(`No supported platforms found for type '${normalizedType}'.`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
94
95
|
const platforms = await multiselect({
|
|
95
96
|
message: "Select target platforms:",
|
|
96
|
-
options:
|
|
97
|
+
options: currentPlatformOptions,
|
|
97
98
|
required: true,
|
|
98
99
|
});
|
|
99
100
|
if (isCancel(platforms)) {
|
|
@@ -104,30 +105,33 @@ export async function addSkill(url) {
|
|
|
104
105
|
process.exit(0);
|
|
105
106
|
}
|
|
106
107
|
const selectedPlatforms = platforms;
|
|
107
|
-
// 3. Check for existing
|
|
108
|
-
const
|
|
108
|
+
// 3. Check for existing items
|
|
109
|
+
const existingItems = [];
|
|
109
110
|
for (const platform of selectedPlatforms) {
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const targetBase = supportedPaths[platform];
|
|
112
|
+
if (!targetBase)
|
|
113
|
+
continue;
|
|
114
|
+
for (const item of selectedItems) {
|
|
115
|
+
const targetPath = path.join(targetBase, item);
|
|
112
116
|
if (await fs.pathExists(targetPath)) {
|
|
113
|
-
|
|
117
|
+
existingItems.push({ item, platform });
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
let overwrite = true;
|
|
118
|
-
if (
|
|
122
|
+
if (existingItems.length > 0) {
|
|
119
123
|
const limit = 5;
|
|
120
|
-
const displayList =
|
|
124
|
+
const displayList = existingItems
|
|
121
125
|
.slice(0, limit)
|
|
122
|
-
.map((e) => `${pc.bold(e.
|
|
126
|
+
.map((e) => `${pc.bold(e.item)} (${e.platform})`)
|
|
123
127
|
.join(", ");
|
|
124
|
-
const remainder =
|
|
128
|
+
const remainder = existingItems.length - limit;
|
|
125
129
|
const message = remainder > 0
|
|
126
|
-
? `The following
|
|
127
|
-
: `The following
|
|
130
|
+
? `The following items already exist: ${displayList}, and ${remainder} others.`
|
|
131
|
+
: `The following items already exist: ${displayList}.`;
|
|
128
132
|
note(message, "Attention");
|
|
129
133
|
const shouldOverwrite = await confirm({
|
|
130
|
-
message: "Do you want to overwrite existing
|
|
134
|
+
message: "Do you want to overwrite existing items?",
|
|
131
135
|
initialValue: false,
|
|
132
136
|
});
|
|
133
137
|
if (isCancel(shouldOverwrite)) {
|
|
@@ -139,26 +143,60 @@ export async function addSkill(url) {
|
|
|
139
143
|
overwrite = shouldOverwrite;
|
|
140
144
|
}
|
|
141
145
|
// 4. Confirmation Note
|
|
142
|
-
note(`Installing ${
|
|
146
|
+
note(`Installing ${selectedItems.length} ${normalizedType}s to ${selectedPlatforms.length} platforms...`, "Summary");
|
|
143
147
|
// 5. Installation Loop with Spinner
|
|
144
148
|
const s = spinner();
|
|
145
|
-
s.start("Installing
|
|
149
|
+
s.start("Installing...");
|
|
146
150
|
const errors = [];
|
|
147
151
|
let installedCount = 0;
|
|
148
152
|
let skippedCount = 0;
|
|
149
153
|
for (const platform of selectedPlatforms) {
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
const targetBase = supportedPaths[platform];
|
|
155
|
+
if (!targetBase)
|
|
156
|
+
continue;
|
|
157
|
+
for (const item of selectedItems) {
|
|
158
|
+
const message = `Installing ${pc.bold(item)} to ${pc.cyan(platform)}...`;
|
|
152
159
|
s.message(message);
|
|
153
160
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
:
|
|
161
|
-
|
|
161
|
+
if (url && tempDir) {
|
|
162
|
+
// We already fetched it, check if it was a file
|
|
163
|
+
// Wait, fetchSkillFromGitHub returned isFile, but add command logic separated fetch from install loop.
|
|
164
|
+
// We need to store isFile state from fetch if using URL.
|
|
165
|
+
// However, selectedItems only stores strings.
|
|
166
|
+
// Let's assume if it is a URL, we re-verify or change logic slightly.
|
|
167
|
+
// Better: modify fetch logic above to store isFile.
|
|
168
|
+
}
|
|
169
|
+
// Re-implementing logic to be consistent with import.ts
|
|
170
|
+
// But wait, add command handles multiple items if LOCAL.
|
|
171
|
+
// If URL, it is single item `selectedItems = [result.skillName]`.
|
|
172
|
+
let currentSourcePath;
|
|
173
|
+
if (url && tempDir) {
|
|
174
|
+
// For URL fetch, we need to know if it was a file.
|
|
175
|
+
// We can check if tempDir/item exists as file.
|
|
176
|
+
const potentialFile = path.join(tempDir, item);
|
|
177
|
+
if ((await fs.pathExists(potentialFile)) &&
|
|
178
|
+
(await fs.stat(potentialFile)).isFile()) {
|
|
179
|
+
currentSourcePath = potentialFile;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
currentSourcePath = tempDir;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Local source
|
|
187
|
+
const src = path.join(TYPE_DIRS[normalizedType], item);
|
|
188
|
+
if ((await fs.stat(src)).isFile()) {
|
|
189
|
+
currentSourcePath = src;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
currentSourcePath = src; // direct path to dir
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let targetItemName = item;
|
|
196
|
+
if (platform === "windsurf" && normalizedType === "agent") {
|
|
197
|
+
targetItemName = path.join(path.parse(item).name, "AGENTS.md");
|
|
198
|
+
}
|
|
199
|
+
const installed = await installItem(targetItemName, overwrite, currentSourcePath, targetBase);
|
|
162
200
|
if (installed) {
|
|
163
201
|
installedCount++;
|
|
164
202
|
}
|
|
@@ -168,7 +206,7 @@ export async function addSkill(url) {
|
|
|
168
206
|
}
|
|
169
207
|
catch (err) {
|
|
170
208
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
171
|
-
errors.push(`${
|
|
209
|
+
errors.push(`${item} -> ${platform}: ${errorMessage}`);
|
|
172
210
|
}
|
|
173
211
|
}
|
|
174
212
|
}
|
|
@@ -184,7 +222,7 @@ export async function addSkill(url) {
|
|
|
184
222
|
}
|
|
185
223
|
}
|
|
186
224
|
else {
|
|
187
|
-
s.stop(pc.green(`Successfully installed ${installedCount}
|
|
225
|
+
s.stop(pc.green(`Successfully installed ${installedCount} ${normalizedType}s. (${skippedCount} skipped)`));
|
|
188
226
|
}
|
|
189
227
|
outro("You're all set!");
|
|
190
228
|
}
|
|
@@ -1,42 +1,80 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import { cancel, confirm, intro, isCancel, outro, spinner, } from "@clack/prompts";
|
|
4
3
|
import fs from "fs-extra";
|
|
5
4
|
import pc from "picocolors";
|
|
6
5
|
import { fetchSkillFromGitHub } from "../utils/github.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { TYPE_DIRS } from "../utils/paths.js";
|
|
7
|
+
export async function importItem(type, url) {
|
|
8
|
+
intro(pc.bgCyan(pc.black(` AI Manager : Import ${type} `)));
|
|
9
|
+
// Normalize type
|
|
10
|
+
const normalizedType = type.toLowerCase().endsWith("s")
|
|
11
|
+
? type.slice(0, -1)
|
|
12
|
+
: type;
|
|
13
|
+
const targetBaseDir = TYPE_DIRS[normalizedType];
|
|
14
|
+
if (!targetBaseDir) {
|
|
15
|
+
cancel(`Unknown type: ${type}. Supported: skill, agent, workflow`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
14
18
|
if (!url) {
|
|
15
19
|
cancel("GitHub URL is required.");
|
|
16
20
|
process.exit(1);
|
|
17
21
|
}
|
|
18
22
|
let tempDir = null;
|
|
19
23
|
try {
|
|
20
|
-
// 1. Fetch
|
|
24
|
+
// 1. Fetch Item (using fetchSkillFromGitHub as generic fetcher)
|
|
21
25
|
const s = spinner();
|
|
22
|
-
s.start(
|
|
23
|
-
let
|
|
26
|
+
s.start(`Fetching ${normalizedType} from GitHub...`);
|
|
27
|
+
let itemName = "";
|
|
28
|
+
let isFile = false;
|
|
24
29
|
try {
|
|
25
30
|
const result = await fetchSkillFromGitHub(url);
|
|
26
31
|
tempDir = result.tempDir;
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
if (!tempDir)
|
|
33
|
+
throw new Error("Failed to create temp directory");
|
|
34
|
+
itemName = result.skillName; // reuse property or rename if possible. assuming generic repo structure
|
|
35
|
+
isFile = result.isFile;
|
|
36
|
+
s.stop(pc.green(`Fetched: ${itemName}`));
|
|
29
37
|
}
|
|
30
38
|
catch (e) {
|
|
31
|
-
s.stop(pc.red("Failed to fetch
|
|
39
|
+
s.stop(pc.red("Failed to fetch"));
|
|
32
40
|
throw e;
|
|
33
41
|
}
|
|
34
42
|
// 2. Determine Target
|
|
35
|
-
const
|
|
43
|
+
const shouldFlatten = (normalizedType === "agent" || normalizedType === "workflow") && !isFile;
|
|
44
|
+
const targetPath = shouldFlatten
|
|
45
|
+
? targetBaseDir
|
|
46
|
+
: path.join(targetBaseDir, itemName);
|
|
36
47
|
// 3. Check Existence & Confirm Overwrite
|
|
37
|
-
if (
|
|
48
|
+
if (shouldFlatten) {
|
|
49
|
+
const srcFiles = await fs.readdir(tempDir);
|
|
50
|
+
const conflicts = [];
|
|
51
|
+
for (const file of srcFiles) {
|
|
52
|
+
if (await fs.pathExists(path.join(targetBaseDir, file))) {
|
|
53
|
+
conflicts.push(file);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (conflicts.length > 0) {
|
|
57
|
+
const limit = 5;
|
|
58
|
+
const displayList = conflicts.slice(0, limit).join(", ");
|
|
59
|
+
const remainder = conflicts.length - limit;
|
|
60
|
+
const message = remainder > 0
|
|
61
|
+
? `Files ${displayList} (and ${remainder} others) already exist in ${targetBaseDir}. Overwrite?`
|
|
62
|
+
: `Files ${displayList} already exist in ${targetBaseDir}. Overwrite?`;
|
|
63
|
+
const shouldOverwrite = await confirm({
|
|
64
|
+
message,
|
|
65
|
+
initialValue: false,
|
|
66
|
+
});
|
|
67
|
+
if (isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
68
|
+
if (tempDir)
|
|
69
|
+
await fs.remove(tempDir);
|
|
70
|
+
cancel("Operation cancelled.");
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (await fs.pathExists(targetPath)) {
|
|
38
76
|
const shouldOverwrite = await confirm({
|
|
39
|
-
message:
|
|
77
|
+
message: `${normalizedType} '${itemName}' already exists in the repo. Overwrite?`,
|
|
40
78
|
initialValue: false,
|
|
41
79
|
});
|
|
42
80
|
if (isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
@@ -46,16 +84,28 @@ export async function importSkill(url) {
|
|
|
46
84
|
process.exit(0);
|
|
47
85
|
}
|
|
48
86
|
}
|
|
49
|
-
// 4. Move/Copy to
|
|
50
|
-
s.start(`Importing ${
|
|
51
|
-
await fs.ensureDir(
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
// 4. Move/Copy to Target Directory
|
|
88
|
+
s.start(`Importing ${itemName} to ${targetBaseDir}...`);
|
|
89
|
+
await fs.ensureDir(targetBaseDir);
|
|
90
|
+
if (isFile) {
|
|
91
|
+
await fs.copy(path.join(tempDir, itemName), targetPath, {
|
|
92
|
+
overwrite: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
if (shouldFlatten) {
|
|
97
|
+
await fs.copy(tempDir, targetBaseDir, { overwrite: true });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await fs.copy(tempDir, targetPath, { overwrite: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
s.stop(pc.green(`Successfully imported ${itemName}!`));
|
|
54
104
|
// Cleanup
|
|
55
105
|
if (tempDir) {
|
|
56
106
|
await fs.remove(tempDir);
|
|
57
107
|
}
|
|
58
|
-
outro(
|
|
108
|
+
outro(`${normalizedType} available at: ${targetPath}`);
|
|
59
109
|
}
|
|
60
110
|
catch (error) {
|
|
61
111
|
if (tempDir)
|