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.
- package/bin/ak.js +1 -90
- package/dist/index.js +2437 -0
- package/dist/index.js.map +1 -0
- package/package.json +20 -10
- package/src/commands/add.js +0 -126
- package/src/commands/doctor.js +0 -129
- package/src/commands/help.js +0 -1412
- package/src/commands/init.js +0 -263
- package/src/commands/list.js +0 -190
- package/src/commands/status.js +0 -113
- package/src/commands/update.js +0 -183
- package/src/index.js +0 -8
- package/src/kits/index.js +0 -122
- package/src/utils/copy.js +0 -214
- package/src/utils/hash.js +0 -74
- package/src/utils/paths.js +0 -255
- package/src/utils/prompts.js +0 -254
- package/src/utils/state.js +0 -136
package/src/utils/paths.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/prompts.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/state.js
DELETED
|
@@ -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
|
-
}
|