apero-kit-cli 1.0.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/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Apero Kit CLI - Main exports
2
+ export { initCommand } from './commands/init.js';
3
+ export { addCommand } from './commands/add.js';
4
+ export { listCommand } from './commands/list.js';
5
+ export { updateCommand } from './commands/update.js';
6
+ export { statusCommand } from './commands/status.js';
7
+ export { doctorCommand } from './commands/doctor.js';
8
+ export { KITS, getKit, getKitNames, getKitList } from './kits/index.js';
@@ -0,0 +1,122 @@
1
+ // Kit definitions - what each kit includes
2
+
3
+ export const KITS = {
4
+ engineer: {
5
+ name: 'engineer',
6
+ description: 'Full-stack development kit for building applications',
7
+ emoji: '🛠️',
8
+ color: 'blue',
9
+ agents: [
10
+ 'planner',
11
+ 'debugger',
12
+ 'fullstack-developer',
13
+ 'tester',
14
+ 'code-reviewer',
15
+ 'git-manager',
16
+ 'database-admin'
17
+ ],
18
+ commands: [
19
+ 'plan', 'plan/parallel', 'plan/fast', 'plan/hard',
20
+ 'code', 'code/auto', 'code/parallel',
21
+ 'fix', 'fix/test', 'fix/types', 'fix/fast', 'fix/ci',
22
+ 'test', 'test/ui',
23
+ 'review', 'review/codebase',
24
+ 'scout', 'build', 'lint'
25
+ ],
26
+ skills: [
27
+ 'frontend-development',
28
+ 'backend-development',
29
+ 'databases',
30
+ 'debugging',
31
+ 'code-review',
32
+ 'planning',
33
+ 'problem-solving'
34
+ ],
35
+ workflows: ['feature-development', 'bug-fixing'],
36
+ includeRouter: true,
37
+ includeHooks: true
38
+ },
39
+
40
+ researcher: {
41
+ name: 'researcher',
42
+ description: 'Research and analysis kit for exploring codebases',
43
+ emoji: '🔬',
44
+ color: 'green',
45
+ agents: [
46
+ 'researcher',
47
+ 'scout',
48
+ 'scout-external',
49
+ 'brainstormer',
50
+ 'docs-manager',
51
+ 'planner'
52
+ ],
53
+ commands: [
54
+ 'scout', 'scout/ext',
55
+ 'investigate', 'brainstorm',
56
+ 'docs', 'docs/init', 'docs/update', 'docs/summarize',
57
+ 'plan', 'ask', 'context'
58
+ ],
59
+ skills: [
60
+ 'research',
61
+ 'planning-with-files',
62
+ 'documentation',
63
+ 'project-index'
64
+ ],
65
+ workflows: [],
66
+ includeRouter: true,
67
+ includeHooks: false
68
+ },
69
+
70
+ designer: {
71
+ name: 'designer',
72
+ description: 'UI/UX design and frontend development kit',
73
+ emoji: '🎨',
74
+ color: 'magenta',
75
+ agents: [
76
+ 'ui-ux-designer',
77
+ 'fullstack-developer',
78
+ 'code-reviewer'
79
+ ],
80
+ commands: [
81
+ 'code', 'fix', 'fix/ui', 'test/ui', 'review'
82
+ ],
83
+ skills: [
84
+ 'ui-ux-pro-max',
85
+ 'frontend-development',
86
+ 'frontend-design'
87
+ ],
88
+ workflows: [],
89
+ includeRouter: false,
90
+ includeHooks: true
91
+ },
92
+
93
+ minimal: {
94
+ name: 'minimal',
95
+ description: 'Lightweight kit with essential agents only',
96
+ emoji: '📦',
97
+ color: 'yellow',
98
+ agents: ['planner', 'debugger'],
99
+ commands: ['plan', 'fix', 'code'],
100
+ skills: ['planning', 'debugging'],
101
+ workflows: [],
102
+ includeRouter: false,
103
+ includeHooks: false
104
+ },
105
+
106
+ full: {
107
+ name: 'full',
108
+ description: 'Complete kit with ALL agents, commands, and skills',
109
+ emoji: '🚀',
110
+ color: 'cyan',
111
+ agents: 'all',
112
+ commands: 'all',
113
+ skills: 'all',
114
+ workflows: 'all',
115
+ includeRouter: true,
116
+ includeHooks: true
117
+ }
118
+ };
119
+
120
+ export const getKit = (name) => KITS[name] || null;
121
+ export const getKitNames = () => Object.keys(KITS);
122
+ export const getKitList = () => Object.values(KITS);
@@ -0,0 +1,194 @@
1
+ import fs from 'fs-extra';
2
+ import { join, basename } from 'path';
3
+
4
+ /**
5
+ * Copy specific items from source to destination
6
+ */
7
+ export async function copyItems(items, type, sourceDir, destDir) {
8
+ const typeDir = join(sourceDir, type);
9
+ const destTypeDir = join(destDir, type);
10
+
11
+ if (!fs.existsSync(typeDir)) {
12
+ return { copied: [], skipped: items, errors: [] };
13
+ }
14
+
15
+ await fs.ensureDir(destTypeDir);
16
+
17
+ const copied = [];
18
+ const skipped = [];
19
+ const errors = [];
20
+
21
+ for (const item of items) {
22
+ try {
23
+ // Handle nested paths like "plan/parallel"
24
+ const itemPath = join(typeDir, item);
25
+ const itemPathMd = itemPath + '.md';
26
+
27
+ let srcPath;
28
+ if (fs.existsSync(itemPath)) {
29
+ srcPath = itemPath;
30
+ } else if (fs.existsSync(itemPathMd)) {
31
+ srcPath = itemPathMd;
32
+ } else {
33
+ skipped.push(item);
34
+ continue;
35
+ }
36
+
37
+ // Determine destination path
38
+ const stat = fs.statSync(srcPath);
39
+ if (stat.isDirectory()) {
40
+ await fs.copy(srcPath, join(destTypeDir, item), { overwrite: true });
41
+ } else {
42
+ // Preserve directory structure for nested items
43
+ const destPath = srcPath.endsWith('.md')
44
+ ? join(destTypeDir, item + '.md')
45
+ : join(destTypeDir, item);
46
+ await fs.ensureDir(join(destTypeDir, item.split('/').slice(0, -1).join('/')));
47
+ await fs.copy(srcPath, destPath, { overwrite: true });
48
+ }
49
+
50
+ copied.push(item);
51
+ } catch (err) {
52
+ errors.push({ item, error: err.message });
53
+ }
54
+ }
55
+
56
+ return { copied, skipped, errors };
57
+ }
58
+
59
+ /**
60
+ * Copy all items of a type
61
+ */
62
+ export async function copyAllOfType(type, sourceDir, destDir) {
63
+ const typeDir = join(sourceDir, type);
64
+ const destTypeDir = join(destDir, type);
65
+
66
+ if (!fs.existsSync(typeDir)) {
67
+ return { success: false, error: `${type} directory not found` };
68
+ }
69
+
70
+ try {
71
+ await fs.copy(typeDir, destTypeDir, { overwrite: true });
72
+ return { success: true };
73
+ } catch (err) {
74
+ return { success: false, error: err.message };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Copy router directory
80
+ */
81
+ export async function copyRouter(sourceDir, destDir) {
82
+ const routerDir = join(sourceDir, 'router');
83
+
84
+ if (!fs.existsSync(routerDir)) {
85
+ return { success: false, error: 'Router directory not found' };
86
+ }
87
+
88
+ try {
89
+ await fs.copy(routerDir, join(destDir, 'router'), { overwrite: true });
90
+ return { success: true };
91
+ } catch (err) {
92
+ return { success: false, error: err.message };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Copy hooks directory
98
+ */
99
+ export async function copyHooks(sourceDir, destDir) {
100
+ const hooksDir = join(sourceDir, 'hooks');
101
+
102
+ if (!fs.existsSync(hooksDir)) {
103
+ return { success: false, error: 'Hooks directory not found' };
104
+ }
105
+
106
+ try {
107
+ await fs.copy(hooksDir, join(destDir, 'hooks'), { overwrite: true });
108
+ return { success: true };
109
+ } catch (err) {
110
+ return { success: false, error: err.message };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Copy workflows directory
116
+ */
117
+ export async function copyWorkflows(items, sourceDir, destDir) {
118
+ if (items === 'all') {
119
+ return copyAllOfType('workflows', sourceDir, destDir);
120
+ }
121
+ return copyItems(items, 'workflows', sourceDir, destDir);
122
+ }
123
+
124
+ /**
125
+ * Copy base files (README, settings, etc.)
126
+ */
127
+ export async function copyBaseFiles(sourceDir, destDir) {
128
+ const baseFiles = ['README.md', 'settings.json', '.env.example'];
129
+ const copied = [];
130
+
131
+ for (const file of baseFiles) {
132
+ const srcPath = join(sourceDir, file);
133
+ if (fs.existsSync(srcPath)) {
134
+ await fs.copy(srcPath, join(destDir, file), { overwrite: true });
135
+ copied.push(file);
136
+ }
137
+ }
138
+
139
+ return copied;
140
+ }
141
+
142
+ /**
143
+ * Copy AGENTS.md to project root
144
+ */
145
+ export async function copyAgentsMd(agentsMdPath, projectDir) {
146
+ if (!agentsMdPath || !fs.existsSync(agentsMdPath)) {
147
+ return false;
148
+ }
149
+
150
+ await fs.copy(agentsMdPath, join(projectDir, 'AGENTS.md'), { overwrite: true });
151
+ return true;
152
+ }
153
+
154
+ /**
155
+ * List available items of a type
156
+ */
157
+ export function listAvailable(type, sourceDir) {
158
+ const typeDir = join(sourceDir, type);
159
+
160
+ if (!fs.existsSync(typeDir)) {
161
+ return [];
162
+ }
163
+
164
+ const items = fs.readdirSync(typeDir);
165
+ return items.map(item => {
166
+ const itemPath = join(typeDir, item);
167
+ const isDir = fs.statSync(itemPath).isDirectory();
168
+ const name = item.replace(/\.md$/, '');
169
+ return { name, isDir, path: itemPath };
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Copy only unchanged files (for update)
175
+ */
176
+ export async function copyUnchangedOnly(sourceDir, destDir, unchangedFiles) {
177
+ const copied = [];
178
+ const skipped = [];
179
+
180
+ for (const file of unchangedFiles) {
181
+ const srcPath = join(sourceDir, file);
182
+ const destPath = join(destDir, file);
183
+
184
+ if (fs.existsSync(srcPath)) {
185
+ await fs.ensureDir(join(destDir, file.split('/').slice(0, -1).join('/')));
186
+ await fs.copy(srcPath, destPath, { overwrite: true });
187
+ copied.push(file);
188
+ } else {
189
+ skipped.push(file);
190
+ }
191
+ }
192
+
193
+ return { copied, skipped };
194
+ }
@@ -0,0 +1,74 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs-extra';
3
+ import { join, relative } from 'path';
4
+
5
+ /**
6
+ * Calculate MD5 hash of a file
7
+ */
8
+ export function hashFile(filePath) {
9
+ if (!fs.existsSync(filePath)) {
10
+ return null;
11
+ }
12
+
13
+ const content = fs.readFileSync(filePath);
14
+ return createHash('md5').update(content).digest('hex');
15
+ }
16
+
17
+ /**
18
+ * Calculate hashes for all files in a directory
19
+ */
20
+ export async function hashDirectory(dirPath, baseDir = dirPath) {
21
+ const hashes = {};
22
+
23
+ if (!fs.existsSync(dirPath)) {
24
+ return hashes;
25
+ }
26
+
27
+ const items = await fs.readdir(dirPath, { withFileTypes: true });
28
+
29
+ for (const item of items) {
30
+ const itemPath = join(dirPath, item.name);
31
+ const relativePath = relative(baseDir, itemPath);
32
+
33
+ if (item.isDirectory()) {
34
+ const subHashes = await hashDirectory(itemPath, baseDir);
35
+ Object.assign(hashes, subHashes);
36
+ } else if (item.isFile()) {
37
+ hashes[relativePath] = hashFile(itemPath);
38
+ }
39
+ }
40
+
41
+ return hashes;
42
+ }
43
+
44
+ /**
45
+ * Compare two hash maps and return differences
46
+ */
47
+ export function compareHashes(original, current) {
48
+ const result = {
49
+ unchanged: [],
50
+ modified: [],
51
+ added: [],
52
+ deleted: []
53
+ };
54
+
55
+ // Check original files
56
+ for (const [path, hash] of Object.entries(original)) {
57
+ if (current[path] === undefined) {
58
+ result.deleted.push(path);
59
+ } else if (current[path] !== hash) {
60
+ result.modified.push(path);
61
+ } else {
62
+ result.unchanged.push(path);
63
+ }
64
+ }
65
+
66
+ // Check for new files
67
+ for (const path of Object.keys(current)) {
68
+ if (original[path] === undefined) {
69
+ result.added.push(path);
70
+ }
71
+ }
72
+
73
+ return result;
74
+ }
@@ -0,0 +1,166 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { existsSync, statSync } from 'fs';
4
+ import { execSync } from 'child_process';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // CLI root directory
10
+ export const CLI_ROOT = resolve(__dirname, '../..');
11
+
12
+ // Target folder mappings
13
+ export const TARGETS = {
14
+ claude: '.claude',
15
+ opencode: '.opencode',
16
+ generic: '.agent'
17
+ };
18
+
19
+ /**
20
+ * Find source directory by traversing up from cwd
21
+ * Algorithm: cwd → parent → git root
22
+ * Looks for: AGENTS.md file or .claude/ directory
23
+ */
24
+ export function findSource(startDir = process.cwd()) {
25
+ let current = resolve(startDir);
26
+ const root = getGitRoot(current) || '/';
27
+
28
+ while (current !== root && current !== '/') {
29
+ // Check for AGENTS.md file
30
+ const agentsMd = join(current, 'AGENTS.md');
31
+ if (existsSync(agentsMd) && statSync(agentsMd).isFile()) {
32
+ // Found AGENTS.md, check for .claude/ in same directory
33
+ const claudeDir = join(current, '.claude');
34
+ if (existsSync(claudeDir) && statSync(claudeDir).isDirectory()) {
35
+ return {
36
+ path: current,
37
+ type: 'agents-repo',
38
+ claudeDir,
39
+ agentsMd
40
+ };
41
+ }
42
+ }
43
+
44
+ // Check for standalone .claude/ directory
45
+ const claudeDir = join(current, '.claude');
46
+ if (existsSync(claudeDir) && statSync(claudeDir).isDirectory()) {
47
+ return {
48
+ path: current,
49
+ type: 'claude-only',
50
+ claudeDir,
51
+ agentsMd: null
52
+ };
53
+ }
54
+
55
+ // Check for .opencode/ as fallback
56
+ const opencodeDir = join(current, '.opencode');
57
+ if (existsSync(opencodeDir) && statSync(opencodeDir).isDirectory()) {
58
+ return {
59
+ path: current,
60
+ type: 'opencode',
61
+ claudeDir: opencodeDir,
62
+ agentsMd: existsSync(join(current, 'AGENTS.md')) ? join(current, 'AGENTS.md') : null
63
+ };
64
+ }
65
+
66
+ current = dirname(current);
67
+ }
68
+
69
+ // Check git root as final attempt
70
+ if (root && root !== '/') {
71
+ const claudeDir = join(root, '.claude');
72
+ if (existsSync(claudeDir)) {
73
+ return {
74
+ path: root,
75
+ type: 'git-root',
76
+ claudeDir,
77
+ agentsMd: existsSync(join(root, 'AGENTS.md')) ? join(root, 'AGENTS.md') : null
78
+ };
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Get git root directory
87
+ */
88
+ export function getGitRoot(startDir = process.cwd()) {
89
+ try {
90
+ const result = execSync('git rev-parse --show-toplevel', {
91
+ cwd: startDir,
92
+ encoding: 'utf-8',
93
+ stdio: ['pipe', 'pipe', 'pipe']
94
+ });
95
+ return result.trim();
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Check if current directory is an ak project
103
+ */
104
+ export function isAkProject(dir = process.cwd()) {
105
+ const akConfig = join(dir, '.ak', 'state.json');
106
+ const claudeDir = join(dir, '.claude');
107
+ const opencodeDir = join(dir, '.opencode');
108
+ const agentDir = join(dir, '.agent');
109
+
110
+ return existsSync(akConfig) ||
111
+ existsSync(claudeDir) ||
112
+ existsSync(opencodeDir) ||
113
+ existsSync(agentDir);
114
+ }
115
+
116
+ /**
117
+ * Get target directory path
118
+ */
119
+ export function getTargetDir(projectDir, target = 'claude') {
120
+ const folder = TARGETS[target] || TARGETS.claude;
121
+ return join(projectDir, folder);
122
+ }
123
+
124
+ /**
125
+ * Resolve source path (from --source flag or auto-detect)
126
+ */
127
+ export function resolveSource(sourceFlag) {
128
+ if (sourceFlag) {
129
+ const resolved = resolve(sourceFlag);
130
+ if (!existsSync(resolved)) {
131
+ return { error: `Source path not found: ${sourceFlag}` };
132
+ }
133
+
134
+ // Check if it's a valid source
135
+ const claudeDir = join(resolved, '.claude');
136
+ const opencodeDir = join(resolved, '.opencode');
137
+
138
+ if (existsSync(claudeDir)) {
139
+ return {
140
+ path: resolved,
141
+ claudeDir,
142
+ agentsMd: existsSync(join(resolved, 'AGENTS.md')) ? join(resolved, 'AGENTS.md') : null
143
+ };
144
+ }
145
+
146
+ if (existsSync(opencodeDir)) {
147
+ return {
148
+ path: resolved,
149
+ claudeDir: opencodeDir,
150
+ agentsMd: existsSync(join(resolved, 'AGENTS.md')) ? join(resolved, 'AGENTS.md') : null
151
+ };
152
+ }
153
+
154
+ return { error: `No .claude/ or .opencode/ found in: ${sourceFlag}` };
155
+ }
156
+
157
+ // Auto-detect
158
+ const found = findSource();
159
+ if (!found) {
160
+ return {
161
+ error: 'Could not find source. Use --source flag or ensure AGENTS.md/.claude/ exists in parent directories.'
162
+ };
163
+ }
164
+
165
+ return found;
166
+ }