apero-kit-cli 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apero-kit-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool to scaffold AI agent projects with pre-configured kits (Claude, OpenCode, Codex)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,30 +15,64 @@ import {
15
15
  promptCommands,
16
16
  promptIncludeRouter,
17
17
  promptIncludeHooks,
18
- promptConfirm
18
+ promptConfirm,
19
+ promptExistingTarget
19
20
  } from '../utils/prompts.js';
20
21
 
21
22
  export async function initCommand(projectName, options) {
22
23
  console.log('');
23
24
 
24
- // 1. Get project name
25
- if (!projectName) {
26
- projectName = await promptProjectName();
25
+ // 1. Get project name (support current directory with "." or empty)
26
+ let projectDir;
27
+ let isCurrentDir = false;
28
+
29
+ if (!projectName || projectName === '.') {
30
+ // Use current directory
31
+ projectDir = process.cwd();
32
+ projectName = '.';
33
+ isCurrentDir = true;
34
+ console.log(chalk.gray(`Initializing in current directory: ${projectDir}`));
35
+ } else {
36
+ projectDir = resolve(process.cwd(), projectName);
27
37
  }
28
38
 
29
- const projectDir = resolve(process.cwd(), projectName);
39
+ // 2. Get target early to check existing
40
+ let target = options.target || 'claude';
41
+ if (!TARGETS[target]) {
42
+ console.log(chalk.yellow(`Unknown target "${target}", using "claude"`));
43
+ target = 'claude';
44
+ }
45
+
46
+ const targetDir = getTargetDir(projectDir, target);
47
+ let existingAction = null;
48
+
49
+ // Check if target directory (.claude, .opencode, etc.) already exists
50
+ if (fs.existsSync(targetDir) && !options.force) {
51
+ if (!process.stdin.isTTY) {
52
+ // Non-interactive mode - skip
53
+ console.log(chalk.yellow(`${TARGETS[target]} already exists. Use --force to override.`));
54
+ return;
55
+ }
56
+
57
+ existingAction = await promptExistingTarget(TARGETS[target]);
58
+
59
+ if (existingAction === 'skip') {
60
+ console.log(chalk.yellow('Skipped. No changes made.'));
61
+ return;
62
+ }
63
+ }
30
64
 
31
- // Check if directory exists
32
- if (fs.existsSync(projectDir) && !options.force) {
65
+ // For new project directory, check if it exists
66
+ if (!isCurrentDir && fs.existsSync(projectDir) && !options.force) {
33
67
  const files = fs.readdirSync(projectDir);
34
- if (files.length > 0) {
68
+ if (files.length > 0 && !existingAction) {
35
69
  console.log(chalk.red(`Directory "${projectName}" already exists and is not empty.`));
36
70
  console.log(chalk.gray('Use --force to overwrite.'));
37
71
  return;
38
72
  }
39
73
  }
40
74
 
41
- // 2. Resolve source
75
+ // 3. Resolve source
42
76
  const source = resolveSource(options.source);
43
77
  if (source.error) {
44
78
  console.log(chalk.red(`Error: ${source.error}`));
@@ -47,20 +81,18 @@ export async function initCommand(projectName, options) {
47
81
 
48
82
  console.log(chalk.gray(`Source: ${source.path}`));
49
83
 
50
- // 3. Get kit
84
+ // 4. Get kit
51
85
  let kitName = options.kit;
52
- if (!kitName) {
86
+ if (!kitName && !options.force) {
53
87
  kitName = await promptKit();
88
+ } else if (!kitName) {
89
+ kitName = 'engineer'; // Default kit for --force mode
54
90
  }
55
91
 
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
- }
92
+ // 5. Set merge mode based on existing action
93
+ const mergeMode = existingAction === 'merge';
62
94
 
63
- // 5. Prepare what to install
95
+ // 6. Prepare what to install
64
96
  let toInstall = {
65
97
  agents: [],
66
98
  commands: [],
@@ -134,56 +166,56 @@ export async function initCommand(projectName, options) {
134
166
  await fs.ensureDir(targetDir);
135
167
 
136
168
  // Copy agents
137
- spinner.text = 'Copying agents...';
169
+ spinner.text = mergeMode ? 'Merging agents...' : 'Copying agents...';
138
170
  if (toInstall.agents === 'all') {
139
- await copyAllOfType('agents', source.claudeDir, targetDir);
171
+ await copyAllOfType('agents', source.claudeDir, targetDir, mergeMode);
140
172
  } else if (toInstall.agents.length > 0) {
141
- await copyItems(toInstall.agents, 'agents', source.claudeDir, targetDir);
173
+ await copyItems(toInstall.agents, 'agents', source.claudeDir, targetDir, mergeMode);
142
174
  }
143
175
 
144
176
  // Copy skills
145
- spinner.text = 'Copying skills...';
177
+ spinner.text = mergeMode ? 'Merging skills...' : 'Copying skills...';
146
178
  if (toInstall.skills === 'all') {
147
- await copyAllOfType('skills', source.claudeDir, targetDir);
179
+ await copyAllOfType('skills', source.claudeDir, targetDir, mergeMode);
148
180
  } else if (toInstall.skills.length > 0) {
149
- await copyItems(toInstall.skills, 'skills', source.claudeDir, targetDir);
181
+ await copyItems(toInstall.skills, 'skills', source.claudeDir, targetDir, mergeMode);
150
182
  }
151
183
 
152
184
  // Copy commands
153
- spinner.text = 'Copying commands...';
185
+ spinner.text = mergeMode ? 'Merging commands...' : 'Copying commands...';
154
186
  if (toInstall.commands === 'all') {
155
- await copyAllOfType('commands', source.claudeDir, targetDir);
187
+ await copyAllOfType('commands', source.claudeDir, targetDir, mergeMode);
156
188
  } else if (toInstall.commands.length > 0) {
157
- await copyItems(toInstall.commands, 'commands', source.claudeDir, targetDir);
189
+ await copyItems(toInstall.commands, 'commands', source.claudeDir, targetDir, mergeMode);
158
190
  }
159
191
 
160
192
  // Copy workflows
161
- spinner.text = 'Copying workflows...';
193
+ spinner.text = mergeMode ? 'Merging workflows...' : 'Copying workflows...';
162
194
  if (toInstall.workflows === 'all') {
163
- await copyAllOfType('workflows', source.claudeDir, targetDir);
195
+ await copyAllOfType('workflows', source.claudeDir, targetDir, mergeMode);
164
196
  } else if (toInstall.workflows && toInstall.workflows.length > 0) {
165
- await copyItems(toInstall.workflows, 'workflows', source.claudeDir, targetDir);
197
+ await copyItems(toInstall.workflows, 'workflows', source.claudeDir, targetDir, mergeMode);
166
198
  }
167
199
 
168
200
  // Copy router
169
201
  if (toInstall.includeRouter) {
170
- spinner.text = 'Copying router...';
171
- await copyRouter(source.claudeDir, targetDir);
202
+ spinner.text = mergeMode ? 'Merging router...' : 'Copying router...';
203
+ await copyRouter(source.claudeDir, targetDir, mergeMode);
172
204
  }
173
205
 
174
206
  // Copy hooks
175
207
  if (toInstall.includeHooks) {
176
- spinner.text = 'Copying hooks...';
177
- await copyHooks(source.claudeDir, targetDir);
208
+ spinner.text = mergeMode ? 'Merging hooks...' : 'Copying hooks...';
209
+ await copyHooks(source.claudeDir, targetDir, mergeMode);
178
210
  }
179
211
 
180
212
  // Copy base files
181
- spinner.text = 'Copying base files...';
182
- await copyBaseFiles(source.claudeDir, targetDir);
213
+ spinner.text = mergeMode ? 'Merging base files...' : 'Copying base files...';
214
+ await copyBaseFiles(source.claudeDir, targetDir, mergeMode);
183
215
 
184
216
  // Copy AGENTS.md
185
217
  if (source.agentsMd) {
186
- await copyAgentsMd(source.agentsMd, projectDir);
218
+ await copyAgentsMd(source.agentsMd, projectDir, mergeMode);
187
219
  }
188
220
 
189
221
  // Create state file
@@ -202,13 +234,18 @@ export async function initCommand(projectName, options) {
202
234
  }
203
235
  });
204
236
 
205
- spinner.succeed(chalk.green('Project created successfully!'));
237
+ const actionWord = mergeMode ? 'merged' : (existingAction === 'override' ? 'overridden' : 'created');
238
+ spinner.succeed(chalk.green(`Project ${actionWord} successfully!`));
206
239
 
207
240
  // Print next steps
208
241
  console.log('');
209
- console.log(chalk.cyan('Next steps:'));
210
- console.log(chalk.white(` cd ${projectName}`));
211
- console.log(chalk.white(' # Start coding with Claude Code'));
242
+ if (!isCurrentDir) {
243
+ console.log(chalk.cyan('Next steps:'));
244
+ console.log(chalk.white(` cd ${projectName}`));
245
+ console.log(chalk.white(' # Start coding with Claude Code'));
246
+ } else {
247
+ console.log(chalk.cyan('Ready to code with Claude Code!'));
248
+ }
212
249
  console.log('');
213
250
  console.log(chalk.gray('Useful commands:'));
214
251
  console.log(chalk.gray(' ak status - Check file status'));
package/src/utils/copy.js CHANGED
@@ -3,8 +3,9 @@ import { join, basename } from 'path';
3
3
 
4
4
  /**
5
5
  * Copy specific items from source to destination
6
+ * @param {boolean} mergeMode - If true, skip existing files
6
7
  */
7
- export async function copyItems(items, type, sourceDir, destDir) {
8
+ export async function copyItems(items, type, sourceDir, destDir, mergeMode = false) {
8
9
  const typeDir = join(sourceDir, type);
9
10
  const destTypeDir = join(destDir, type);
10
11
 
@@ -37,14 +38,14 @@ export async function copyItems(items, type, sourceDir, destDir) {
37
38
  // Determine destination path
38
39
  const stat = fs.statSync(srcPath);
39
40
  if (stat.isDirectory()) {
40
- await fs.copy(srcPath, join(destTypeDir, item), { overwrite: true });
41
+ await fs.copy(srcPath, join(destTypeDir, item), { overwrite: !mergeMode });
41
42
  } else {
42
43
  // Preserve directory structure for nested items
43
44
  const destPath = srcPath.endsWith('.md')
44
45
  ? join(destTypeDir, item + '.md')
45
46
  : join(destTypeDir, item);
46
47
  await fs.ensureDir(join(destTypeDir, item.split('/').slice(0, -1).join('/')));
47
- await fs.copy(srcPath, destPath, { overwrite: true });
48
+ await fs.copy(srcPath, destPath, { overwrite: !mergeMode });
48
49
  }
49
50
 
50
51
  copied.push(item);
@@ -58,8 +59,9 @@ export async function copyItems(items, type, sourceDir, destDir) {
58
59
 
59
60
  /**
60
61
  * Copy all items of a type
62
+ * @param {boolean} mergeMode - If true, skip existing files
61
63
  */
62
- export async function copyAllOfType(type, sourceDir, destDir) {
64
+ export async function copyAllOfType(type, sourceDir, destDir, mergeMode = false) {
63
65
  const typeDir = join(sourceDir, type);
64
66
  const destTypeDir = join(destDir, type);
65
67
 
@@ -68,7 +70,7 @@ export async function copyAllOfType(type, sourceDir, destDir) {
68
70
  }
69
71
 
70
72
  try {
71
- await fs.copy(typeDir, destTypeDir, { overwrite: true });
73
+ await fs.copy(typeDir, destTypeDir, { overwrite: !mergeMode });
72
74
  return { success: true };
73
75
  } catch (err) {
74
76
  return { success: false, error: err.message };
@@ -77,8 +79,9 @@ export async function copyAllOfType(type, sourceDir, destDir) {
77
79
 
78
80
  /**
79
81
  * Copy router directory
82
+ * @param {boolean} mergeMode - If true, skip existing files
80
83
  */
81
- export async function copyRouter(sourceDir, destDir) {
84
+ export async function copyRouter(sourceDir, destDir, mergeMode = false) {
82
85
  const routerDir = join(sourceDir, 'router');
83
86
 
84
87
  if (!fs.existsSync(routerDir)) {
@@ -86,7 +89,7 @@ export async function copyRouter(sourceDir, destDir) {
86
89
  }
87
90
 
88
91
  try {
89
- await fs.copy(routerDir, join(destDir, 'router'), { overwrite: true });
92
+ await fs.copy(routerDir, join(destDir, 'router'), { overwrite: !mergeMode });
90
93
  return { success: true };
91
94
  } catch (err) {
92
95
  return { success: false, error: err.message };
@@ -95,8 +98,9 @@ export async function copyRouter(sourceDir, destDir) {
95
98
 
96
99
  /**
97
100
  * Copy hooks directory
101
+ * @param {boolean} mergeMode - If true, skip existing files
98
102
  */
99
- export async function copyHooks(sourceDir, destDir) {
103
+ export async function copyHooks(sourceDir, destDir, mergeMode = false) {
100
104
  const hooksDir = join(sourceDir, 'hooks');
101
105
 
102
106
  if (!fs.existsSync(hooksDir)) {
@@ -104,7 +108,7 @@ export async function copyHooks(sourceDir, destDir) {
104
108
  }
105
109
 
106
110
  try {
107
- await fs.copy(hooksDir, join(destDir, 'hooks'), { overwrite: true });
111
+ await fs.copy(hooksDir, join(destDir, 'hooks'), { overwrite: !mergeMode });
108
112
  return { success: true };
109
113
  } catch (err) {
110
114
  return { success: false, error: err.message };
@@ -123,15 +127,23 @@ export async function copyWorkflows(items, sourceDir, destDir) {
123
127
 
124
128
  /**
125
129
  * Copy base files (README, settings, etc.)
130
+ * @param {boolean} mergeMode - If true, skip existing files
126
131
  */
127
- export async function copyBaseFiles(sourceDir, destDir) {
132
+ export async function copyBaseFiles(sourceDir, destDir, mergeMode = false) {
128
133
  const baseFiles = ['README.md', 'settings.json', '.env.example'];
129
134
  const copied = [];
130
135
 
131
136
  for (const file of baseFiles) {
132
137
  const srcPath = join(sourceDir, file);
138
+ const destPath = join(destDir, file);
139
+
140
+ // Skip if merge mode and file exists
141
+ if (mergeMode && fs.existsSync(destPath)) {
142
+ continue;
143
+ }
144
+
133
145
  if (fs.existsSync(srcPath)) {
134
- await fs.copy(srcPath, join(destDir, file), { overwrite: true });
146
+ await fs.copy(srcPath, destPath, { overwrite: !mergeMode });
135
147
  copied.push(file);
136
148
  }
137
149
  }
@@ -141,13 +153,21 @@ export async function copyBaseFiles(sourceDir, destDir) {
141
153
 
142
154
  /**
143
155
  * Copy AGENTS.md to project root
156
+ * @param {boolean} mergeMode - If true, skip if file exists
144
157
  */
145
- export async function copyAgentsMd(agentsMdPath, projectDir) {
158
+ export async function copyAgentsMd(agentsMdPath, projectDir, mergeMode = false) {
146
159
  if (!agentsMdPath || !fs.existsSync(agentsMdPath)) {
147
160
  return false;
148
161
  }
149
162
 
150
- await fs.copy(agentsMdPath, join(projectDir, 'AGENTS.md'), { overwrite: true });
163
+ const destPath = join(projectDir, 'AGENTS.md');
164
+
165
+ // Skip if merge mode and file exists
166
+ if (mergeMode && fs.existsSync(destPath)) {
167
+ return false;
168
+ }
169
+
170
+ await fs.copy(agentsMdPath, destPath, { overwrite: !mergeMode });
151
171
  return true;
152
172
  }
153
173
 
@@ -207,6 +207,25 @@ export async function promptConfirm(message, defaultValue = true) {
207
207
  return confirmed;
208
208
  }
209
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
+
210
229
  /**
211
230
  * Prompt for update confirmation with file list
212
231
  */