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.
@@ -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
+ }