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,223 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { KITS, getKit } from '../kits/index.js';
|
|
6
|
+
import { resolveSource, getTargetDir, TARGETS } from '../utils/paths.js';
|
|
7
|
+
import { copyItems, copyAllOfType, copyRouter, copyHooks, copyBaseFiles, copyAgentsMd } from '../utils/copy.js';
|
|
8
|
+
import { createInitialState } from '../utils/state.js';
|
|
9
|
+
import {
|
|
10
|
+
promptProjectName,
|
|
11
|
+
promptKit,
|
|
12
|
+
promptTarget,
|
|
13
|
+
promptAgents,
|
|
14
|
+
promptSkills,
|
|
15
|
+
promptCommands,
|
|
16
|
+
promptIncludeRouter,
|
|
17
|
+
promptIncludeHooks,
|
|
18
|
+
promptConfirm
|
|
19
|
+
} from '../utils/prompts.js';
|
|
20
|
+
|
|
21
|
+
export async function initCommand(projectName, options) {
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
// 1. Get project name
|
|
25
|
+
if (!projectName) {
|
|
26
|
+
projectName = await promptProjectName();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const projectDir = resolve(process.cwd(), projectName);
|
|
30
|
+
|
|
31
|
+
// Check if directory exists
|
|
32
|
+
if (fs.existsSync(projectDir) && !options.force) {
|
|
33
|
+
const files = fs.readdirSync(projectDir);
|
|
34
|
+
if (files.length > 0) {
|
|
35
|
+
console.log(chalk.red(`Directory "${projectName}" already exists and is not empty.`));
|
|
36
|
+
console.log(chalk.gray('Use --force to overwrite.'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Resolve source
|
|
42
|
+
const source = resolveSource(options.source);
|
|
43
|
+
if (source.error) {
|
|
44
|
+
console.log(chalk.red(`Error: ${source.error}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(chalk.gray(`Source: ${source.path}`));
|
|
49
|
+
|
|
50
|
+
// 3. Get kit
|
|
51
|
+
let kitName = options.kit;
|
|
52
|
+
if (!kitName) {
|
|
53
|
+
kitName = await promptKit();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. Get target
|
|
57
|
+
let target = options.target || 'claude';
|
|
58
|
+
if (!TARGETS[target]) {
|
|
59
|
+
console.log(chalk.yellow(`Unknown target "${target}", using "claude"`));
|
|
60
|
+
target = 'claude';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. Prepare what to install
|
|
64
|
+
let toInstall = {
|
|
65
|
+
agents: [],
|
|
66
|
+
commands: [],
|
|
67
|
+
skills: [],
|
|
68
|
+
workflows: [],
|
|
69
|
+
includeRouter: false,
|
|
70
|
+
includeHooks: false
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (kitName === 'custom') {
|
|
74
|
+
// Custom mode - prompt for everything
|
|
75
|
+
console.log(chalk.cyan('\nCustom kit configuration:'));
|
|
76
|
+
toInstall.agents = await promptAgents(source.claudeDir);
|
|
77
|
+
toInstall.skills = await promptSkills(source.claudeDir);
|
|
78
|
+
toInstall.commands = await promptCommands(source.claudeDir);
|
|
79
|
+
toInstall.includeRouter = await promptIncludeRouter();
|
|
80
|
+
toInstall.includeHooks = await promptIncludeHooks();
|
|
81
|
+
} else {
|
|
82
|
+
const kit = getKit(kitName);
|
|
83
|
+
if (!kit) {
|
|
84
|
+
console.log(chalk.red(`Unknown kit: ${kitName}`));
|
|
85
|
+
console.log(chalk.gray(`Available kits: ${Object.keys(KITS).join(', ')}`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
toInstall = {
|
|
90
|
+
agents: kit.agents,
|
|
91
|
+
commands: kit.commands,
|
|
92
|
+
skills: kit.skills,
|
|
93
|
+
workflows: kit.workflows || [],
|
|
94
|
+
includeRouter: kit.includeRouter,
|
|
95
|
+
includeHooks: kit.includeHooks
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 6. Confirm
|
|
100
|
+
console.log(chalk.cyan('\nWill create:'));
|
|
101
|
+
console.log(chalk.white(` Project: ${projectName}/`));
|
|
102
|
+
console.log(chalk.white(` Target: ${TARGETS[target]}/`));
|
|
103
|
+
console.log(chalk.white(` Kit: ${kitName}`));
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(toInstall.agents)) {
|
|
106
|
+
console.log(chalk.gray(` Agents: ${toInstall.agents.length}`));
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(toInstall.skills)) {
|
|
109
|
+
console.log(chalk.gray(` Skills: ${toInstall.skills.length}`));
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(toInstall.commands)) {
|
|
112
|
+
console.log(chalk.gray(` Commands: ${toInstall.commands.length}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
if (!await promptConfirm('Proceed?')) {
|
|
118
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 7. Create project
|
|
123
|
+
const spinner = ora('Creating project...').start();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Create project directory
|
|
127
|
+
await fs.ensureDir(projectDir);
|
|
128
|
+
|
|
129
|
+
// Create target directory
|
|
130
|
+
const targetDir = getTargetDir(projectDir, target);
|
|
131
|
+
await fs.ensureDir(targetDir);
|
|
132
|
+
|
|
133
|
+
// Copy agents
|
|
134
|
+
spinner.text = 'Copying agents...';
|
|
135
|
+
if (toInstall.agents === 'all') {
|
|
136
|
+
await copyAllOfType('agents', source.claudeDir, targetDir);
|
|
137
|
+
} else if (toInstall.agents.length > 0) {
|
|
138
|
+
await copyItems(toInstall.agents, 'agents', source.claudeDir, targetDir);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Copy skills
|
|
142
|
+
spinner.text = 'Copying skills...';
|
|
143
|
+
if (toInstall.skills === 'all') {
|
|
144
|
+
await copyAllOfType('skills', source.claudeDir, targetDir);
|
|
145
|
+
} else if (toInstall.skills.length > 0) {
|
|
146
|
+
await copyItems(toInstall.skills, 'skills', source.claudeDir, targetDir);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Copy commands
|
|
150
|
+
spinner.text = 'Copying commands...';
|
|
151
|
+
if (toInstall.commands === 'all') {
|
|
152
|
+
await copyAllOfType('commands', source.claudeDir, targetDir);
|
|
153
|
+
} else if (toInstall.commands.length > 0) {
|
|
154
|
+
await copyItems(toInstall.commands, 'commands', source.claudeDir, targetDir);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Copy workflows
|
|
158
|
+
spinner.text = 'Copying workflows...';
|
|
159
|
+
if (toInstall.workflows === 'all') {
|
|
160
|
+
await copyAllOfType('workflows', source.claudeDir, targetDir);
|
|
161
|
+
} else if (toInstall.workflows && toInstall.workflows.length > 0) {
|
|
162
|
+
await copyItems(toInstall.workflows, 'workflows', source.claudeDir, targetDir);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Copy router
|
|
166
|
+
if (toInstall.includeRouter) {
|
|
167
|
+
spinner.text = 'Copying router...';
|
|
168
|
+
await copyRouter(source.claudeDir, targetDir);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Copy hooks
|
|
172
|
+
if (toInstall.includeHooks) {
|
|
173
|
+
spinner.text = 'Copying hooks...';
|
|
174
|
+
await copyHooks(source.claudeDir, targetDir);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Copy base files
|
|
178
|
+
spinner.text = 'Copying base files...';
|
|
179
|
+
await copyBaseFiles(source.claudeDir, targetDir);
|
|
180
|
+
|
|
181
|
+
// Copy AGENTS.md
|
|
182
|
+
if (source.agentsMd) {
|
|
183
|
+
await copyAgentsMd(source.agentsMd, projectDir);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create state file
|
|
187
|
+
spinner.text = 'Saving state...';
|
|
188
|
+
await createInitialState(projectDir, {
|
|
189
|
+
kit: kitName,
|
|
190
|
+
source: source.path,
|
|
191
|
+
target: TARGETS[target],
|
|
192
|
+
installed: {
|
|
193
|
+
agents: toInstall.agents === 'all' ? ['all'] : toInstall.agents,
|
|
194
|
+
skills: toInstall.skills === 'all' ? ['all'] : toInstall.skills,
|
|
195
|
+
commands: toInstall.commands === 'all' ? ['all'] : toInstall.commands,
|
|
196
|
+
workflows: toInstall.workflows === 'all' ? ['all'] : (toInstall.workflows || []),
|
|
197
|
+
router: toInstall.includeRouter,
|
|
198
|
+
hooks: toInstall.includeHooks
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
spinner.succeed(chalk.green('Project created successfully!'));
|
|
203
|
+
|
|
204
|
+
// Print next steps
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.cyan('Next steps:'));
|
|
207
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
208
|
+
console.log(chalk.white(' # Start coding with Claude Code'));
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log(chalk.gray('Useful commands:'));
|
|
211
|
+
console.log(chalk.gray(' ak status - Check file status'));
|
|
212
|
+
console.log(chalk.gray(' ak add <item> - Add more agents/skills'));
|
|
213
|
+
console.log(chalk.gray(' ak update - Sync from source'));
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
spinner.fail(chalk.red('Failed to create project'));
|
|
218
|
+
console.error(chalk.red(error.message));
|
|
219
|
+
if (process.env.DEBUG) {
|
|
220
|
+
console.error(error.stack);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { KITS, getKitList } from '../kits/index.js';
|
|
3
|
+
import { resolveSource } from '../utils/paths.js';
|
|
4
|
+
import { listAvailable } from '../utils/copy.js';
|
|
5
|
+
|
|
6
|
+
export async function listCommand(type, options = {}) {
|
|
7
|
+
const validTypes = ['kits', 'agents', 'skills', 'commands', 'workflows'];
|
|
8
|
+
|
|
9
|
+
// If no type specified, show help
|
|
10
|
+
if (!type) {
|
|
11
|
+
console.log(chalk.cyan('\nAvailable list commands:'));
|
|
12
|
+
console.log(chalk.white(' ak list kits - List available kits'));
|
|
13
|
+
console.log(chalk.white(' ak list agents - List available agents'));
|
|
14
|
+
console.log(chalk.white(' ak list skills - List available skills'));
|
|
15
|
+
console.log(chalk.white(' ak list commands - List available commands'));
|
|
16
|
+
console.log(chalk.white(' ak list workflows - List available workflows'));
|
|
17
|
+
console.log('');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Normalize type
|
|
22
|
+
type = type.toLowerCase();
|
|
23
|
+
if (!validTypes.includes(type)) {
|
|
24
|
+
console.log(chalk.red(`Unknown type: ${type}`));
|
|
25
|
+
console.log(chalk.gray(`Valid types: ${validTypes.join(', ')}`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// List kits (doesn't need source)
|
|
30
|
+
if (type === 'kits') {
|
|
31
|
+
listKits();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// For other types, need source
|
|
36
|
+
const source = resolveSource(options.source);
|
|
37
|
+
if (source.error) {
|
|
38
|
+
console.log(chalk.red(`Error: ${source.error}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(chalk.gray(`Source: ${source.path}\n`));
|
|
43
|
+
|
|
44
|
+
switch (type) {
|
|
45
|
+
case 'agents':
|
|
46
|
+
listAgents(source.claudeDir);
|
|
47
|
+
break;
|
|
48
|
+
case 'skills':
|
|
49
|
+
listSkills(source.claudeDir);
|
|
50
|
+
break;
|
|
51
|
+
case 'commands':
|
|
52
|
+
listCommands(source.claudeDir);
|
|
53
|
+
break;
|
|
54
|
+
case 'workflows':
|
|
55
|
+
listWorkflows(source.claudeDir);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listKits() {
|
|
61
|
+
const kits = getKitList();
|
|
62
|
+
|
|
63
|
+
console.log(chalk.cyan.bold('\nAvailable Kits:\n'));
|
|
64
|
+
|
|
65
|
+
for (const kit of kits) {
|
|
66
|
+
const colorFn = chalk[kit.color] || chalk.white;
|
|
67
|
+
console.log(` ${kit.emoji} ${colorFn.bold(kit.name.padEnd(12))} - ${kit.description}`);
|
|
68
|
+
|
|
69
|
+
// Show details
|
|
70
|
+
if (Array.isArray(kit.agents)) {
|
|
71
|
+
console.log(chalk.gray(` Agents: ${kit.agents.length} | Skills: ${kit.skills.length} | Commands: ${kit.commands.length}`));
|
|
72
|
+
} else {
|
|
73
|
+
console.log(chalk.gray(' Includes: ALL agents, skills, commands'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\n 🔧 ${chalk.bold('custom'.padEnd(12))} - Pick your own agents, skills, and commands`);
|
|
78
|
+
console.log('');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listAgents(sourceDir) {
|
|
82
|
+
const agents = listAvailable('agents', sourceDir);
|
|
83
|
+
|
|
84
|
+
console.log(chalk.cyan.bold(`Available Agents (${agents.length}):\n`));
|
|
85
|
+
|
|
86
|
+
if (agents.length === 0) {
|
|
87
|
+
console.log(chalk.gray(' No agents found'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Group into columns
|
|
92
|
+
const cols = 3;
|
|
93
|
+
const rows = Math.ceil(agents.length / cols);
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < rows; i++) {
|
|
96
|
+
let line = ' ';
|
|
97
|
+
for (let j = 0; j < cols; j++) {
|
|
98
|
+
const idx = i + j * rows;
|
|
99
|
+
if (idx < agents.length) {
|
|
100
|
+
line += chalk.white(agents[idx].name.padEnd(25));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log(line);
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function listSkills(sourceDir) {
|
|
109
|
+
const skills = listAvailable('skills', sourceDir).filter(s => s.isDir);
|
|
110
|
+
|
|
111
|
+
console.log(chalk.cyan.bold(`Available Skills (${skills.length}):\n`));
|
|
112
|
+
|
|
113
|
+
if (skills.length === 0) {
|
|
114
|
+
console.log(chalk.gray(' No skills found'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Group into columns
|
|
119
|
+
const cols = 2;
|
|
120
|
+
const rows = Math.ceil(skills.length / cols);
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < rows; i++) {
|
|
123
|
+
let line = ' ';
|
|
124
|
+
for (let j = 0; j < cols; j++) {
|
|
125
|
+
const idx = i + j * rows;
|
|
126
|
+
if (idx < skills.length) {
|
|
127
|
+
line += chalk.white(skills[idx].name.padEnd(35));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(line);
|
|
131
|
+
}
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function listCommands(sourceDir) {
|
|
136
|
+
const commands = listAvailable('commands', sourceDir);
|
|
137
|
+
|
|
138
|
+
console.log(chalk.cyan.bold(`Available Commands (${commands.length}):\n`));
|
|
139
|
+
|
|
140
|
+
if (commands.length === 0) {
|
|
141
|
+
console.log(chalk.gray(' No commands found'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Separate files and directories
|
|
146
|
+
const files = commands.filter(c => !c.isDir);
|
|
147
|
+
const dirs = commands.filter(c => c.isDir);
|
|
148
|
+
|
|
149
|
+
// Print main commands (files)
|
|
150
|
+
console.log(chalk.gray(' Main commands:'));
|
|
151
|
+
const cols = 4;
|
|
152
|
+
let rows = Math.ceil(files.length / cols);
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < rows; i++) {
|
|
155
|
+
let line = ' ';
|
|
156
|
+
for (let j = 0; j < cols; j++) {
|
|
157
|
+
const idx = i + j * rows;
|
|
158
|
+
if (idx < files.length) {
|
|
159
|
+
line += chalk.white(('/' + files[idx].name).padEnd(18));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(line);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Print command groups (directories)
|
|
166
|
+
if (dirs.length > 0) {
|
|
167
|
+
console.log(chalk.gray('\n Command groups:'));
|
|
168
|
+
for (const dir of dirs) {
|
|
169
|
+
console.log(chalk.yellow(` /${dir.name}/`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function listWorkflows(sourceDir) {
|
|
177
|
+
const workflows = listAvailable('workflows', sourceDir);
|
|
178
|
+
|
|
179
|
+
console.log(chalk.cyan.bold(`Available Workflows (${workflows.length}):\n`));
|
|
180
|
+
|
|
181
|
+
if (workflows.length === 0) {
|
|
182
|
+
console.log(chalk.gray(' No workflows found'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const wf of workflows) {
|
|
187
|
+
console.log(chalk.white(` • ${wf.name}`));
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { isAkProject } from '../utils/paths.js';
|
|
3
|
+
import { loadState, getFileStatuses } from '../utils/state.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
export async function statusCommand(options = {}) {
|
|
8
|
+
const projectDir = process.cwd();
|
|
9
|
+
|
|
10
|
+
// Check if in ak project
|
|
11
|
+
if (!isAkProject(projectDir)) {
|
|
12
|
+
console.log(chalk.red('Not in an ak project.'));
|
|
13
|
+
console.log(chalk.gray('Run "ak init" first.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Load state
|
|
18
|
+
const state = await loadState(projectDir);
|
|
19
|
+
if (!state) {
|
|
20
|
+
console.log(chalk.yellow('No state file found.'));
|
|
21
|
+
console.log(chalk.gray('This project may have been created without ak.'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Print project info
|
|
26
|
+
console.log(chalk.cyan.bold('\nProject Status\n'));
|
|
27
|
+
console.log(chalk.white(` Kit: ${state.kit}`));
|
|
28
|
+
console.log(chalk.white(` Target: ${state.target}`));
|
|
29
|
+
console.log(chalk.white(` Source: ${state.source}`));
|
|
30
|
+
console.log(chalk.gray(` Created: ${new Date(state.createdAt).toLocaleDateString()}`));
|
|
31
|
+
console.log(chalk.gray(` Updated: ${new Date(state.lastUpdate).toLocaleDateString()}`));
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
// Get file statuses
|
|
35
|
+
const result = await getFileStatuses(projectDir);
|
|
36
|
+
|
|
37
|
+
if (result.error) {
|
|
38
|
+
console.log(chalk.red(`Error: ${result.error}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { statuses } = result;
|
|
43
|
+
|
|
44
|
+
// Summary
|
|
45
|
+
const total = statuses.unchanged.length + statuses.modified.length + statuses.added.length;
|
|
46
|
+
console.log(chalk.cyan('File Status:'));
|
|
47
|
+
console.log(chalk.green(` ✓ ${statuses.unchanged.length} unchanged`));
|
|
48
|
+
if (statuses.modified.length > 0) {
|
|
49
|
+
console.log(chalk.yellow(` ~ ${statuses.modified.length} modified`));
|
|
50
|
+
}
|
|
51
|
+
if (statuses.added.length > 0) {
|
|
52
|
+
console.log(chalk.blue(` + ${statuses.added.length} added locally`));
|
|
53
|
+
}
|
|
54
|
+
if (statuses.deleted.length > 0) {
|
|
55
|
+
console.log(chalk.red(` - ${statuses.deleted.length} deleted`));
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.gray(` Total: ${total} files tracked`));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
// Show modified files
|
|
61
|
+
if (statuses.modified.length > 0 && (options.verbose || statuses.modified.length <= 10)) {
|
|
62
|
+
console.log(chalk.yellow('Modified files:'));
|
|
63
|
+
for (const file of statuses.modified) {
|
|
64
|
+
console.log(chalk.yellow(` ~ ${file}`));
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
} else if (statuses.modified.length > 10) {
|
|
68
|
+
console.log(chalk.yellow(`Modified files: (showing first 10, use --verbose for all)`));
|
|
69
|
+
for (const file of statuses.modified.slice(0, 10)) {
|
|
70
|
+
console.log(chalk.yellow(` ~ ${file}`));
|
|
71
|
+
}
|
|
72
|
+
console.log(chalk.gray(` ... and ${statuses.modified.length - 10} more`));
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Show added files
|
|
77
|
+
if (statuses.added.length > 0 && (options.verbose || statuses.added.length <= 5)) {
|
|
78
|
+
console.log(chalk.blue('Added locally:'));
|
|
79
|
+
for (const file of statuses.added) {
|
|
80
|
+
console.log(chalk.blue(` + ${file}`));
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Installed components
|
|
86
|
+
if (state.installed) {
|
|
87
|
+
console.log(chalk.cyan('Installed Components:'));
|
|
88
|
+
const { agents, skills, commands, workflows, router, hooks } = state.installed;
|
|
89
|
+
|
|
90
|
+
if (agents && agents.length > 0) {
|
|
91
|
+
console.log(chalk.gray(` Agents: ${agents.includes('all') ? 'ALL' : agents.length}`));
|
|
92
|
+
}
|
|
93
|
+
if (skills && skills.length > 0) {
|
|
94
|
+
console.log(chalk.gray(` Skills: ${skills.includes('all') ? 'ALL' : skills.length}`));
|
|
95
|
+
}
|
|
96
|
+
if (commands && commands.length > 0) {
|
|
97
|
+
console.log(chalk.gray(` Commands: ${commands.includes('all') ? 'ALL' : commands.length}`));
|
|
98
|
+
}
|
|
99
|
+
if (workflows && workflows.length > 0) {
|
|
100
|
+
console.log(chalk.gray(` Workflows: ${workflows.includes('all') ? 'ALL' : workflows.length}`));
|
|
101
|
+
}
|
|
102
|
+
if (router) console.log(chalk.gray(' Router: ✓'));
|
|
103
|
+
if (hooks) console.log(chalk.gray(' Hooks: ✓'));
|
|
104
|
+
console.log('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check source availability
|
|
108
|
+
if (state.source && !fs.existsSync(state.source)) {
|
|
109
|
+
console.log(chalk.yellow('⚠ Source directory not found. Update may not work.'));
|
|
110
|
+
console.log(chalk.gray(` Expected: ${state.source}`));
|
|
111
|
+
console.log('');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { resolveSource, isAkProject } from '../utils/paths.js';
|
|
6
|
+
import { loadState, updateState, getFileStatuses } from '../utils/state.js';
|
|
7
|
+
import { hashFile, hashDirectory } from '../utils/hash.js';
|
|
8
|
+
import { promptUpdateConfirm } from '../utils/prompts.js';
|
|
9
|
+
|
|
10
|
+
export async function updateCommand(options = {}) {
|
|
11
|
+
const projectDir = process.cwd();
|
|
12
|
+
|
|
13
|
+
// Check if in ak project
|
|
14
|
+
if (!isAkProject(projectDir)) {
|
|
15
|
+
console.log(chalk.red('Not in an ak project.'));
|
|
16
|
+
console.log(chalk.gray('Run "ak init" first.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Load state
|
|
21
|
+
const state = await loadState(projectDir);
|
|
22
|
+
if (!state) {
|
|
23
|
+
console.log(chalk.red('No state file found.'));
|
|
24
|
+
console.log(chalk.gray('This project may have been created without ak. Run "ak doctor" for more info.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Resolve source
|
|
29
|
+
const sourceFlag = options.source || state.source;
|
|
30
|
+
const source = resolveSource(sourceFlag);
|
|
31
|
+
if (source.error) {
|
|
32
|
+
console.log(chalk.red(`Error: ${source.error}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.gray(`Source: ${source.path}`));
|
|
37
|
+
console.log(chalk.gray(`Target: ${state.target}`));
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
const spinner = ora('Checking for updates...').start();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Get current file statuses
|
|
44
|
+
const { statuses, targetDir } = await getFileStatuses(projectDir);
|
|
45
|
+
|
|
46
|
+
// Determine what types to update
|
|
47
|
+
let typesToUpdate = ['agents', 'skills', 'commands', 'workflows'];
|
|
48
|
+
if (options.agents) typesToUpdate = ['agents'];
|
|
49
|
+
if (options.skills) typesToUpdate = ['skills'];
|
|
50
|
+
if (options.commands) typesToUpdate = ['commands'];
|
|
51
|
+
|
|
52
|
+
// Get source hashes
|
|
53
|
+
const sourceHashes = {};
|
|
54
|
+
for (const type of typesToUpdate) {
|
|
55
|
+
const typeDir = join(source.claudeDir, type);
|
|
56
|
+
if (fs.existsSync(typeDir)) {
|
|
57
|
+
const hashes = await hashDirectory(typeDir);
|
|
58
|
+
for (const [path, hash] of Object.entries(hashes)) {
|
|
59
|
+
sourceHashes[`${type}/${path}`] = hash;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Compare with current
|
|
65
|
+
const toUpdate = [];
|
|
66
|
+
const skipped = [];
|
|
67
|
+
const newFiles = [];
|
|
68
|
+
|
|
69
|
+
for (const [path, sourceHash] of Object.entries(sourceHashes)) {
|
|
70
|
+
const currentPath = join(targetDir, path);
|
|
71
|
+
const originalHash = state.originalHashes?.[path];
|
|
72
|
+
const currentHash = fs.existsSync(currentPath) ? hashFile(currentPath) : null;
|
|
73
|
+
|
|
74
|
+
if (!currentHash) {
|
|
75
|
+
// New file from source
|
|
76
|
+
newFiles.push(path);
|
|
77
|
+
} else if (currentHash === originalHash) {
|
|
78
|
+
// File unchanged locally, can update
|
|
79
|
+
if (sourceHash !== currentHash) {
|
|
80
|
+
toUpdate.push(path);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// File modified locally, skip
|
|
84
|
+
if (sourceHash !== originalHash) {
|
|
85
|
+
skipped.push(path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
spinner.stop();
|
|
91
|
+
|
|
92
|
+
// Show summary
|
|
93
|
+
if (toUpdate.length === 0 && newFiles.length === 0) {
|
|
94
|
+
console.log(chalk.green('✓ Already up to date!'));
|
|
95
|
+
if (skipped.length > 0) {
|
|
96
|
+
console.log(chalk.yellow(` ${skipped.length} file(s) skipped (modified locally)`));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.cyan('Updates available:'));
|
|
102
|
+
console.log(chalk.green(` ${toUpdate.length} file(s) to update`));
|
|
103
|
+
console.log(chalk.blue(` ${newFiles.length} new file(s)`));
|
|
104
|
+
if (skipped.length > 0) {
|
|
105
|
+
console.log(chalk.yellow(` ${skipped.length} file(s) skipped (modified locally)`));
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
// Dry run mode
|
|
110
|
+
if (options.dryRun) {
|
|
111
|
+
console.log(chalk.cyan('Dry run - no changes made'));
|
|
112
|
+
console.log(chalk.gray('\nFiles that would be updated:'));
|
|
113
|
+
[...toUpdate, ...newFiles].forEach(f => console.log(chalk.gray(` ${f}`)));
|
|
114
|
+
if (skipped.length > 0) {
|
|
115
|
+
console.log(chalk.gray('\nFiles that would be skipped:'));
|
|
116
|
+
skipped.forEach(f => console.log(chalk.yellow(` ~ ${f}`)));
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Confirm
|
|
122
|
+
if (!options.force) {
|
|
123
|
+
const confirmed = await promptUpdateConfirm({
|
|
124
|
+
toUpdate: [...toUpdate, ...newFiles],
|
|
125
|
+
skipped
|
|
126
|
+
});
|
|
127
|
+
if (!confirmed) {
|
|
128
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Apply updates
|
|
134
|
+
const updateSpinner = ora('Applying updates...').start();
|
|
135
|
+
|
|
136
|
+
let updated = 0;
|
|
137
|
+
let failed = 0;
|
|
138
|
+
|
|
139
|
+
for (const path of [...toUpdate, ...newFiles]) {
|
|
140
|
+
try {
|
|
141
|
+
const srcPath = join(source.claudeDir, path);
|
|
142
|
+
const destPath = join(targetDir, path);
|
|
143
|
+
|
|
144
|
+
await fs.ensureDir(join(targetDir, path.split('/').slice(0, -1).join('/')));
|
|
145
|
+
await fs.copy(srcPath, destPath, { overwrite: true });
|
|
146
|
+
updated++;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
failed++;
|
|
149
|
+
if (process.env.DEBUG) {
|
|
150
|
+
console.error(`Failed to update ${path}: ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Update state with new hashes
|
|
156
|
+
const newHashes = await hashDirectory(targetDir);
|
|
157
|
+
await updateState(projectDir, {
|
|
158
|
+
source: source.path,
|
|
159
|
+
originalHashes: newHashes
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
updateSpinner.succeed(chalk.green(`Updated ${updated} file(s)`));
|
|
163
|
+
|
|
164
|
+
if (failed > 0) {
|
|
165
|
+
console.log(chalk.yellow(` ${failed} file(s) failed to update`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (skipped.length > 0) {
|
|
169
|
+
console.log(chalk.gray(`\nSkipped files (modified locally):`));
|
|
170
|
+
skipped.slice(0, 5).forEach(f => console.log(chalk.yellow(` ~ ${f}`)));
|
|
171
|
+
if (skipped.length > 5) {
|
|
172
|
+
console.log(chalk.gray(` ... and ${skipped.length - 5} more`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} catch (error) {
|
|
177
|
+
spinner.fail(chalk.red('Update failed'));
|
|
178
|
+
console.error(chalk.red(error.message));
|
|
179
|
+
if (process.env.DEBUG) {
|
|
180
|
+
console.error(error.stack);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|