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/LICENSE +21 -0
- package/README.md +252 -0
- package/bin/ak.js +78 -0
- package/package.json +61 -0
- package/src/commands/add.js +126 -0
- package/src/commands/doctor.js +129 -0
- package/src/commands/init.js +223 -0
- package/src/commands/list.js +190 -0
- package/src/commands/status.js +113 -0
- package/src/commands/update.js +183 -0
- package/src/index.js +8 -0
- package/src/kits/index.js +122 -0
- package/src/utils/copy.js +194 -0
- package/src/utils/hash.js +74 -0
- package/src/utils/paths.js +166 -0
- package/src/utils/prompts.js +235 -0
- package/src/utils/state.js +136 -0
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
|
+
}
|