apero-kit-cli 1.7.1 → 2.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.
@@ -1,255 +0,0 @@
1
- import { fileURLToPath } from 'url';
2
- import { dirname, join, resolve } from 'path';
3
- import { existsSync, statSync, mkdirSync } from 'fs';
4
- import { execSync } from 'child_process';
5
- import { homedir } from 'os';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = dirname(__filename);
9
-
10
- // CLI root directory
11
- export const CLI_ROOT = resolve(__dirname, '../..');
12
-
13
- // Embedded templates directory (inside CLI package)
14
- export const TEMPLATES_DIR = join(CLI_ROOT, 'templates');
15
-
16
- // Remote templates config
17
- const REMOTE_REPO_URL = 'https://github.com/Thanhnguyen6702/CK-Internal.git';
18
- const CACHE_DIR = join(homedir(), '.apero-kit', 'CK-Internal');
19
-
20
- // Target folder mappings
21
- export const TARGETS = {
22
- claude: '.claude',
23
- opencode: '.opencode',
24
- generic: '.agent'
25
- };
26
-
27
- /**
28
- * Fetch or update remote templates from CK-Internal GitHub repo.
29
- * Clones on first run, pulls on subsequent runs.
30
- * Returns source object or null on failure.
31
- */
32
- export function fetchRemoteTemplates() {
33
- try {
34
- const cacheParent = join(homedir(), '.apero-kit');
35
- if (!existsSync(cacheParent)) {
36
- mkdirSync(cacheParent, { recursive: true });
37
- }
38
-
39
- if (existsSync(join(CACHE_DIR, '.git'))) {
40
- // Already cloned — pull latest
41
- try {
42
- execSync('git pull --ff-only', {
43
- cwd: CACHE_DIR,
44
- encoding: 'utf-8',
45
- stdio: ['pipe', 'pipe', 'pipe'],
46
- timeout: 30000
47
- });
48
- } catch {
49
- // pull failed (offline, etc.) — use cached version
50
- }
51
- } else {
52
- // First time — clone
53
- execSync(`git clone --depth 1 "${REMOTE_REPO_URL}" "${CACHE_DIR}"`, {
54
- encoding: 'utf-8',
55
- stdio: ['pipe', 'pipe', 'pipe'],
56
- timeout: 60000
57
- });
58
- }
59
-
60
- const claudeDir = join(CACHE_DIR, '.claude');
61
- if (existsSync(claudeDir) && statSync(claudeDir).isDirectory()) {
62
- const agentsMd = join(CACHE_DIR, 'AGENTS.md');
63
- return {
64
- path: CACHE_DIR,
65
- type: 'remote',
66
- claudeDir,
67
- agentsMd: existsSync(agentsMd) ? agentsMd : null
68
- };
69
- }
70
- } catch {
71
- // Clone failed (no network, etc.) — fall through
72
- }
73
- return null;
74
- }
75
-
76
- /**
77
- * Get embedded templates (bundled with CLI)
78
- */
79
- export function getEmbeddedTemplates() {
80
- if (existsSync(TEMPLATES_DIR)) {
81
- const agentsMd = join(TEMPLATES_DIR, 'AGENTS.md');
82
- return {
83
- path: TEMPLATES_DIR,
84
- type: 'embedded',
85
- claudeDir: TEMPLATES_DIR,
86
- agentsMd: existsSync(agentsMd) ? agentsMd : null
87
- };
88
- }
89
- return null;
90
- }
91
-
92
- /**
93
- * Find source directory by traversing up from cwd
94
- * Algorithm: cwd → parent → git root
95
- * Looks for: AGENTS.md file or .claude/ directory
96
- */
97
- export function findSource(startDir = process.cwd()) {
98
- let current = resolve(startDir);
99
- const root = getGitRoot(current) || '/';
100
-
101
- while (current !== root && current !== '/') {
102
- // Check for AGENTS.md file
103
- const agentsMd = join(current, 'AGENTS.md');
104
- if (existsSync(agentsMd) && statSync(agentsMd).isFile()) {
105
- // Found AGENTS.md, check for .claude/ in same directory
106
- const claudeDir = join(current, '.claude');
107
- if (existsSync(claudeDir) && statSync(claudeDir).isDirectory()) {
108
- return {
109
- path: current,
110
- type: 'agents-repo',
111
- claudeDir,
112
- agentsMd
113
- };
114
- }
115
- }
116
-
117
- // Check for standalone .claude/ directory
118
- const claudeDir = join(current, '.claude');
119
- if (existsSync(claudeDir) && statSync(claudeDir).isDirectory()) {
120
- return {
121
- path: current,
122
- type: 'claude-only',
123
- claudeDir,
124
- agentsMd: null
125
- };
126
- }
127
-
128
- // Check for .opencode/ as fallback
129
- const opencodeDir = join(current, '.opencode');
130
- if (existsSync(opencodeDir) && statSync(opencodeDir).isDirectory()) {
131
- return {
132
- path: current,
133
- type: 'opencode',
134
- claudeDir: opencodeDir,
135
- agentsMd: existsSync(join(current, 'AGENTS.md')) ? join(current, 'AGENTS.md') : null
136
- };
137
- }
138
-
139
- current = dirname(current);
140
- }
141
-
142
- // Check git root as final attempt
143
- if (root && root !== '/') {
144
- const claudeDir = join(root, '.claude');
145
- if (existsSync(claudeDir)) {
146
- return {
147
- path: root,
148
- type: 'git-root',
149
- claudeDir,
150
- agentsMd: existsSync(join(root, 'AGENTS.md')) ? join(root, 'AGENTS.md') : null
151
- };
152
- }
153
- }
154
-
155
- return null;
156
- }
157
-
158
- /**
159
- * Get git root directory
160
- */
161
- export function getGitRoot(startDir = process.cwd()) {
162
- try {
163
- const result = execSync('git rev-parse --show-toplevel', {
164
- cwd: startDir,
165
- encoding: 'utf-8',
166
- stdio: ['pipe', 'pipe', 'pipe']
167
- });
168
- return result.trim();
169
- } catch {
170
- return null;
171
- }
172
- }
173
-
174
- /**
175
- * Check if current directory is an ak project
176
- */
177
- export function isAkProject(dir = process.cwd()) {
178
- const akConfig = join(dir, '.ak', 'state.json');
179
- const claudeDir = join(dir, '.claude');
180
- const opencodeDir = join(dir, '.opencode');
181
- const agentDir = join(dir, '.agent');
182
-
183
- return existsSync(akConfig) ||
184
- existsSync(claudeDir) ||
185
- existsSync(opencodeDir) ||
186
- existsSync(agentDir);
187
- }
188
-
189
- /**
190
- * Get target directory path
191
- */
192
- export function getTargetDir(projectDir, target = 'claude') {
193
- const folder = TARGETS[target] || TARGETS.claude;
194
- return join(projectDir, folder);
195
- }
196
-
197
- /**
198
- * Resolve source path (from --source flag, embedded templates, or auto-detect)
199
- * Priority: 1. --source flag 2. Embedded templates 3. Auto-detect in parent dirs
200
- */
201
- export function resolveSource(sourceFlag) {
202
- // 1. If --source flag provided, use it
203
- if (sourceFlag) {
204
- const resolved = resolve(sourceFlag);
205
- if (!existsSync(resolved)) {
206
- return { error: `Source path not found: ${sourceFlag}` };
207
- }
208
-
209
- // Check if it's a valid source
210
- const claudeDir = join(resolved, '.claude');
211
- const opencodeDir = join(resolved, '.opencode');
212
-
213
- if (existsSync(claudeDir)) {
214
- return {
215
- path: resolved,
216
- type: 'custom',
217
- claudeDir,
218
- agentsMd: existsSync(join(resolved, 'AGENTS.md')) ? join(resolved, 'AGENTS.md') : null
219
- };
220
- }
221
-
222
- if (existsSync(opencodeDir)) {
223
- return {
224
- path: resolved,
225
- type: 'custom',
226
- claudeDir: opencodeDir,
227
- agentsMd: existsSync(join(resolved, 'AGENTS.md')) ? join(resolved, 'AGENTS.md') : null
228
- };
229
- }
230
-
231
- return { error: `No .claude/ or .opencode/ found in: ${sourceFlag}` };
232
- }
233
-
234
- // 2. Fetch from remote CK-Internal repo - PREFERRED
235
- const remote = fetchRemoteTemplates();
236
- if (remote) {
237
- return remote;
238
- }
239
-
240
- // 3. Use embedded templates (bundled with CLI) - FALLBACK
241
- const embedded = getEmbeddedTemplates();
242
- if (embedded) {
243
- return embedded;
244
- }
245
-
246
- // 4. Fallback: auto-detect in parent directories
247
- const found = findSource();
248
- if (found) {
249
- return found;
250
- }
251
-
252
- return {
253
- error: 'No templates found. Check your network connection or try reinstalling: npm install -g apero-kit-cli'
254
- };
255
- }
@@ -1,254 +0,0 @@
1
- import inquirer from 'inquirer';
2
- import chalk from 'chalk';
3
- import { KITS, getKitList } from '../kits/index.js';
4
- import { listAvailable } from './copy.js';
5
-
6
- /**
7
- * Prompt for project name
8
- */
9
- export async function promptProjectName() {
10
- const { projectName } = await inquirer.prompt([
11
- {
12
- type: 'input',
13
- name: 'projectName',
14
- message: 'Project name:',
15
- default: 'my-project',
16
- validate: (input) => {
17
- if (!input.trim()) return 'Project name is required';
18
- if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
19
- return 'Project name can only contain letters, numbers, dashes, and underscores';
20
- }
21
- return true;
22
- }
23
- }
24
- ]);
25
- return projectName;
26
- }
27
-
28
- /**
29
- * Prompt for kit selection
30
- */
31
- export async function promptKit() {
32
- const kits = getKitList();
33
- const choices = kits.map(kit => ({
34
- name: `${kit.emoji} ${chalk.bold(kit.name.padEnd(12))} - ${kit.description}`,
35
- value: kit.name
36
- }));
37
-
38
- // Add custom option
39
- choices.push({
40
- name: `🔧 ${chalk.bold('custom'.padEnd(12))} - Pick your own agents, skills, and commands`,
41
- value: 'custom'
42
- });
43
-
44
- const { kit } = await inquirer.prompt([
45
- {
46
- type: 'list',
47
- name: 'kit',
48
- message: 'Select a kit:',
49
- choices
50
- }
51
- ]);
52
-
53
- return kit;
54
- }
55
-
56
- /**
57
- * Prompt for target folder
58
- */
59
- export async function promptTarget() {
60
- const { target } = await inquirer.prompt([
61
- {
62
- type: 'list',
63
- name: 'target',
64
- message: 'Target folder:',
65
- choices: [
66
- { name: '.claude/ (Claude Code)', value: 'claude' },
67
- { name: '.opencode/ (OpenCode)', value: 'opencode' },
68
- { name: '.agent/ (Generic)', value: 'generic' }
69
- ],
70
- default: 'claude'
71
- }
72
- ]);
73
- return target;
74
- }
75
-
76
- /**
77
- * Prompt for custom agent selection
78
- */
79
- export async function promptAgents(sourceDir) {
80
- const available = listAvailable('agents', sourceDir);
81
-
82
- if (available.length === 0) {
83
- return [];
84
- }
85
-
86
- const choices = available.map(item => ({
87
- name: item.name,
88
- value: item.name,
89
- checked: ['planner', 'debugger'].includes(item.name) // Default selections
90
- }));
91
-
92
- const { agents } = await inquirer.prompt([
93
- {
94
- type: 'checkbox',
95
- name: 'agents',
96
- message: 'Select agents:',
97
- choices,
98
- pageSize: 15
99
- }
100
- ]);
101
-
102
- return agents;
103
- }
104
-
105
- /**
106
- * Prompt for custom skill selection
107
- */
108
- export async function promptSkills(sourceDir) {
109
- const available = listAvailable('skills', sourceDir);
110
-
111
- if (available.length === 0) {
112
- return [];
113
- }
114
-
115
- const choices = available
116
- .filter(item => item.isDir) // Skills are directories
117
- .map(item => ({
118
- name: item.name,
119
- value: item.name,
120
- checked: ['planning', 'debugging'].includes(item.name)
121
- }));
122
-
123
- const { skills } = await inquirer.prompt([
124
- {
125
- type: 'checkbox',
126
- name: 'skills',
127
- message: 'Select skills:',
128
- choices,
129
- pageSize: 15
130
- }
131
- ]);
132
-
133
- return skills;
134
- }
135
-
136
- /**
137
- * Prompt for custom command selection
138
- */
139
- export async function promptCommands(sourceDir) {
140
- const available = listAvailable('commands', sourceDir);
141
-
142
- if (available.length === 0) {
143
- return [];
144
- }
145
-
146
- const choices = available.map(item => ({
147
- name: item.name,
148
- value: item.name,
149
- checked: ['plan', 'fix', 'code'].includes(item.name)
150
- }));
151
-
152
- const { commands } = await inquirer.prompt([
153
- {
154
- type: 'checkbox',
155
- name: 'commands',
156
- message: 'Select commands:',
157
- choices,
158
- pageSize: 15
159
- }
160
- ]);
161
-
162
- return commands;
163
- }
164
-
165
- /**
166
- * Prompt for router inclusion
167
- */
168
- export async function promptIncludeRouter() {
169
- const { includeRouter } = await inquirer.prompt([
170
- {
171
- type: 'confirm',
172
- name: 'includeRouter',
173
- message: 'Include router?',
174
- default: true
175
- }
176
- ]);
177
- return includeRouter;
178
- }
179
-
180
- /**
181
- * Prompt for hooks inclusion
182
- */
183
- export async function promptIncludeHooks() {
184
- const { includeHooks } = await inquirer.prompt([
185
- {
186
- type: 'confirm',
187
- name: 'includeHooks',
188
- message: 'Include hooks?',
189
- default: false
190
- }
191
- ]);
192
- return includeHooks;
193
- }
194
-
195
- /**
196
- * Prompt for confirmation
197
- */
198
- export async function promptConfirm(message, defaultValue = true) {
199
- const { confirmed } = await inquirer.prompt([
200
- {
201
- type: 'confirm',
202
- name: 'confirmed',
203
- message,
204
- default: defaultValue
205
- }
206
- ]);
207
- return confirmed;
208
- }
209
-
210
- /**
211
- * Prompt for existing target directory action
212
- */
213
- export async function promptExistingTarget(targetPath) {
214
- const { action } = await inquirer.prompt([
215
- {
216
- type: 'list',
217
- name: 'action',
218
- message: `${targetPath} already exists. What do you want to do?`,
219
- choices: [
220
- { name: '🔄 Override - Replace all files', value: 'override' },
221
- { name: '📦 Merge - Only add missing files', value: 'merge' },
222
- { name: '⏭️ Skip - Do nothing', value: 'skip' }
223
- ]
224
- }
225
- ]);
226
- return action;
227
- }
228
-
229
- /**
230
- * Prompt for update confirmation with file list
231
- */
232
- export async function promptUpdateConfirm(updates) {
233
- console.log(chalk.cyan('\nChanges to apply:'));
234
-
235
- if (updates.toUpdate.length > 0) {
236
- console.log(chalk.green(' Will update:'));
237
- updates.toUpdate.slice(0, 10).forEach(f => console.log(chalk.green(` ✓ ${f}`)));
238
- if (updates.toUpdate.length > 10) {
239
- console.log(chalk.gray(` ... and ${updates.toUpdate.length - 10} more`));
240
- }
241
- }
242
-
243
- if (updates.skipped.length > 0) {
244
- console.log(chalk.yellow(' Will skip (modified locally):'));
245
- updates.skipped.slice(0, 5).forEach(f => console.log(chalk.yellow(` ~ ${f}`)));
246
- if (updates.skipped.length > 5) {
247
- console.log(chalk.gray(` ... and ${updates.skipped.length - 5} more`));
248
- }
249
- }
250
-
251
- console.log('');
252
-
253
- return promptConfirm('Apply these updates?', true);
254
- }
@@ -1,136 +0,0 @@
1
- import fs from 'fs-extra';
2
- import { join } from 'path';
3
- import { hashDirectory } from './hash.js';
4
-
5
- const STATE_DIR = '.ak';
6
- const STATE_FILE = 'state.json';
7
-
8
- /**
9
- * Get state file path
10
- */
11
- export function getStatePath(projectDir) {
12
- return join(projectDir, STATE_DIR, STATE_FILE);
13
- }
14
-
15
- /**
16
- * Load state from .ak/state.json
17
- */
18
- export async function loadState(projectDir) {
19
- const statePath = getStatePath(projectDir);
20
-
21
- if (!fs.existsSync(statePath)) {
22
- return null;
23
- }
24
-
25
- try {
26
- return await fs.readJson(statePath);
27
- } catch {
28
- return null;
29
- }
30
- }
31
-
32
- /**
33
- * Save state to .ak/state.json
34
- */
35
- export async function saveState(projectDir, state) {
36
- const stateDir = join(projectDir, STATE_DIR);
37
- const statePath = join(stateDir, STATE_FILE);
38
-
39
- await fs.ensureDir(stateDir);
40
- await fs.writeJson(statePath, state, { spaces: 2 });
41
- }
42
-
43
- /**
44
- * Create initial state after init
45
- */
46
- export async function createInitialState(projectDir, options) {
47
- const { kit, source, target, installed } = options;
48
-
49
- // Calculate hashes of all installed files
50
- const targetDir = join(projectDir, target);
51
- const hashes = await hashDirectory(targetDir);
52
-
53
- const state = {
54
- version: '1.0.0',
55
- createdAt: new Date().toISOString(),
56
- lastUpdate: new Date().toISOString(),
57
- kit,
58
- source,
59
- target,
60
- installed,
61
- originalHashes: hashes
62
- };
63
-
64
- await saveState(projectDir, state);
65
- return state;
66
- }
67
-
68
- /**
69
- * Update state after update command
70
- */
71
- export async function updateState(projectDir, updates) {
72
- const state = await loadState(projectDir);
73
-
74
- if (!state) {
75
- throw new Error('No state found. Is this an ak project?');
76
- }
77
-
78
- const targetDir = join(projectDir, state.target || '.claude');
79
- const newHashes = await hashDirectory(targetDir);
80
-
81
- const updatedState = {
82
- ...state,
83
- ...updates,
84
- lastUpdate: new Date().toISOString(),
85
- originalHashes: newHashes
86
- };
87
-
88
- await saveState(projectDir, updatedState);
89
- return updatedState;
90
- }
91
-
92
- /**
93
- * Get file status (unchanged, modified, added, deleted)
94
- */
95
- export async function getFileStatuses(projectDir) {
96
- const state = await loadState(projectDir);
97
-
98
- if (!state) {
99
- return { error: 'No state found' };
100
- }
101
-
102
- const targetDir = join(projectDir, state.target || '.claude');
103
- const currentHashes = await hashDirectory(targetDir);
104
- const originalHashes = state.originalHashes || {};
105
-
106
- const statuses = {
107
- unchanged: [],
108
- modified: [],
109
- added: [],
110
- deleted: []
111
- };
112
-
113
- // Check original files
114
- for (const [path, hash] of Object.entries(originalHashes)) {
115
- if (currentHashes[path] === undefined) {
116
- statuses.deleted.push(path);
117
- } else if (currentHashes[path] !== hash) {
118
- statuses.modified.push(path);
119
- } else {
120
- statuses.unchanged.push(path);
121
- }
122
- }
123
-
124
- // Check for new files
125
- for (const path of Object.keys(currentHashes)) {
126
- if (originalHashes[path] === undefined) {
127
- statuses.added.push(path);
128
- }
129
- }
130
-
131
- return {
132
- state,
133
- statuses,
134
- targetDir
135
- };
136
- }