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
|
@@ -0,0 +1,235 @@
|
|
|
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 update confirmation with file list
|
|
212
|
+
*/
|
|
213
|
+
export async function promptUpdateConfirm(updates) {
|
|
214
|
+
console.log(chalk.cyan('\nChanges to apply:'));
|
|
215
|
+
|
|
216
|
+
if (updates.toUpdate.length > 0) {
|
|
217
|
+
console.log(chalk.green(' Will update:'));
|
|
218
|
+
updates.toUpdate.slice(0, 10).forEach(f => console.log(chalk.green(` ✓ ${f}`)));
|
|
219
|
+
if (updates.toUpdate.length > 10) {
|
|
220
|
+
console.log(chalk.gray(` ... and ${updates.toUpdate.length - 10} more`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (updates.skipped.length > 0) {
|
|
225
|
+
console.log(chalk.yellow(' Will skip (modified locally):'));
|
|
226
|
+
updates.skipped.slice(0, 5).forEach(f => console.log(chalk.yellow(` ~ ${f}`)));
|
|
227
|
+
if (updates.skipped.length > 5) {
|
|
228
|
+
console.log(chalk.gray(` ... and ${updates.skipped.length - 5} more`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('');
|
|
233
|
+
|
|
234
|
+
return promptConfirm('Apply these updates?', true);
|
|
235
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
}
|