apero-kit-cli 1.1.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 +1 -1
- package/src/commands/init.js +78 -41
- package/src/utils/copy.js +33 -13
- package/src/utils/prompts.js +19 -0
- package/templates/commands/plan/fast.md +9 -0
- package/templates/commands/plan/hard.md +9 -0
- package/templates/commands/plan/parallel.md +9 -0
- package/templates/commands/plan/preview.md +40 -0
- package/templates/scripts/plan-preview.cjs +367 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
57
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
|
package/src/utils/prompts.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -66,6 +66,15 @@ Use `planner` subagent to:
|
|
|
66
66
|
- Save the overview access point at `{plan-dir}/plan.md`. Keep it generic, under 80 lines, and list each implementation phase with status and progress plus links to phase files.
|
|
67
67
|
- For each phase, create `{plan-dir}/phase-XX-phase-name-here.md` containing the following sections in order: Context links (reference parent plan, dependencies, docs), Overview (date, description, priority, implementation status, review status), Key Insights, Requirements, Architecture, Related code files, Implementation Steps, Todo list, Success Criteria, Risk Assessment, Security Considerations, Next steps.
|
|
68
68
|
|
|
69
|
+
## Post-Plan Preview (Optional)
|
|
70
|
+
|
|
71
|
+
After plan creation, offer to open in browser for easier reading.
|
|
72
|
+
|
|
73
|
+
Use `AskUserQuestion` tool:
|
|
74
|
+
- "Open plan in browser for easier reading?" ā Yes (Recommended) / No
|
|
75
|
+
|
|
76
|
+
**If user chooses Yes:** Run `/plan:preview {plan-path}` SlashCommand.
|
|
77
|
+
|
|
69
78
|
## Important Notes
|
|
70
79
|
|
|
71
80
|
- **IMPORTANT:** Ensure token consumption efficiency while maintaining high quality.
|
|
@@ -45,6 +45,15 @@ After plan creation, offer validation interview to confirm decisions before impl
|
|
|
45
45
|
**If mode is `prompt`:** Use `AskUserQuestion` tool with options above.
|
|
46
46
|
**If user chooses validation or mode is `auto`:** Execute `/plan:validate {plan-path}` SlashCommand.
|
|
47
47
|
|
|
48
|
+
## Post-Plan Preview (Optional)
|
|
49
|
+
|
|
50
|
+
After plan creation, offer to open in browser for easier reading.
|
|
51
|
+
|
|
52
|
+
Use `AskUserQuestion` tool:
|
|
53
|
+
- "Open plan in browser for easier reading?" ā Yes (Recommended) / No
|
|
54
|
+
|
|
55
|
+
**If user chooses Yes:** Run `/plan:preview {plan-path}` SlashCommand.
|
|
56
|
+
|
|
48
57
|
## Output Requirements
|
|
49
58
|
|
|
50
59
|
**Plan Directory Structure** (use `Plan dir:` from `## Naming` section)
|
|
@@ -131,6 +131,15 @@ Phase 04: Integration Tests (depends on 01, 02, 03)
|
|
|
131
131
|
- Execution strategy (e.g., "Phases 1-3 parallel, then Phase 4")
|
|
132
132
|
- File ownership matrix (which phase owns which files)
|
|
133
133
|
|
|
134
|
+
## Post-Plan Preview (Optional)
|
|
135
|
+
|
|
136
|
+
After plan creation, offer to open in browser for easier reading.
|
|
137
|
+
|
|
138
|
+
Use `AskUserQuestion` tool:
|
|
139
|
+
- "Open plan in browser for easier reading?" ā Yes (Recommended) / No
|
|
140
|
+
|
|
141
|
+
**If user chooses Yes:** Run `/plan:preview {plan-path}` SlashCommand.
|
|
142
|
+
|
|
134
143
|
## Important Notes
|
|
135
144
|
|
|
136
145
|
**IMPORTANT:** Analyze the skills catalog and activate the skills that are needed for the task during the process.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: šļø Open plan in browser for easy reading
|
|
3
|
+
argument-hint: [plan-path]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Your mission
|
|
7
|
+
|
|
8
|
+
Open the plan preview server to view the plan in a nicely formatted web page.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. **Determine plan path:**
|
|
13
|
+
- If `$ARGUMENTS` provided ā Use that path
|
|
14
|
+
- If active plan exists in `## Plan Context` ā Use that path
|
|
15
|
+
- If neither ā Ask user to specify a plan path
|
|
16
|
+
|
|
17
|
+
2. **Start preview server:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node .claude/scripts/plan-preview.cjs {plan-path}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. **Inform user:**
|
|
24
|
+
- Browser will open automatically
|
|
25
|
+
- Server runs on `http://localhost:3456`
|
|
26
|
+
- Press `Ctrl+C` in terminal to stop
|
|
27
|
+
|
|
28
|
+
## Example Usage
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/plan:preview plans/250116-feature-auth
|
|
32
|
+
/plan:preview # Uses active plan from context
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
- The preview server renders markdown with syntax highlighting
|
|
38
|
+
- Navigation sidebar shows all plan files (plan.md, phases, reports)
|
|
39
|
+
- Server auto-detects plan structure (research/, reports/, scout/ subdirs)
|
|
40
|
+
- Refresh the page to see updated content
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Plan Preview Server
|
|
4
|
+
*
|
|
5
|
+
* Usage: node .claude/scripts/plan-preview.cjs <plan-path> [--port=3456]
|
|
6
|
+
*
|
|
7
|
+
* Opens a local web server to preview plans with:
|
|
8
|
+
* - Rendered markdown with syntax highlighting
|
|
9
|
+
* - Navigation sidebar for phases
|
|
10
|
+
* - Auto-refresh on file changes
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { exec } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const planPath = process.argv[2];
|
|
19
|
+
const portArg = process.argv.find(arg => arg.startsWith('--port='));
|
|
20
|
+
const PORT = portArg ? parseInt(portArg.split('=')[1]) : 3456;
|
|
21
|
+
|
|
22
|
+
if (!planPath) {
|
|
23
|
+
console.error('Error: Plan path required');
|
|
24
|
+
console.log('Usage: node .claude/scripts/plan-preview.cjs <plan-path>');
|
|
25
|
+
console.log('Example: node .claude/scripts/plan-preview.cjs plans/251207-feature');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fullPlanPath = path.resolve(process.cwd(), planPath);
|
|
30
|
+
if (!fs.existsSync(fullPlanPath)) {
|
|
31
|
+
console.error(`Error: Plan directory not found: ${fullPlanPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Simple markdown to HTML converter
|
|
36
|
+
function markdownToHtml(md) {
|
|
37
|
+
return md
|
|
38
|
+
// Code blocks with language
|
|
39
|
+
.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
40
|
+
const langClass = lang ? `language-${lang}` : '';
|
|
41
|
+
return `<pre class="${langClass}"><code>${escapeHtml(code.trim())}</code></pre>`;
|
|
42
|
+
})
|
|
43
|
+
// Inline code
|
|
44
|
+
.replace(/`([^`]+)`/g, '<code class="inline">$1</code>')
|
|
45
|
+
// Headers
|
|
46
|
+
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
|
|
47
|
+
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
|
|
48
|
+
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
|
|
49
|
+
// Bold and italic
|
|
50
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
51
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
52
|
+
// Links
|
|
53
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
54
|
+
// Lists
|
|
55
|
+
.replace(/^\s*[-*] (.*$)/gm, '<li>$1</li>')
|
|
56
|
+
.replace(/(<li>.*<\/li>)\n(?!<li>)/g, '$1</ul>\n')
|
|
57
|
+
.replace(/(?<!<\/li>\n)(<li>)/g, '<ul>$1')
|
|
58
|
+
// Numbered lists
|
|
59
|
+
.replace(/^\s*\d+\. (.*$)/gm, '<li class="numbered">$1</li>')
|
|
60
|
+
// Blockquotes
|
|
61
|
+
.replace(/^> (.*$)/gm, '<blockquote>$1</blockquote>')
|
|
62
|
+
// Horizontal rules
|
|
63
|
+
.replace(/^---+$/gm, '<hr>')
|
|
64
|
+
// Paragraphs
|
|
65
|
+
.replace(/\n\n/g, '</p><p>')
|
|
66
|
+
// Tables (simple)
|
|
67
|
+
.replace(/\|(.+)\|/g, (match) => {
|
|
68
|
+
const cells = match.split('|').filter(c => c.trim());
|
|
69
|
+
if (cells.every(c => /^[-:]+$/.test(c.trim()))) return ''; // Skip separator
|
|
70
|
+
const tag = 'td';
|
|
71
|
+
return '<tr>' + cells.map(c => `<${tag}>${c.trim()}</${tag}>`).join('') + '</tr>';
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function escapeHtml(text) {
|
|
76
|
+
return text
|
|
77
|
+
.replace(/&/g, '&')
|
|
78
|
+
.replace(/</g, '<')
|
|
79
|
+
.replace(/>/g, '>')
|
|
80
|
+
.replace(/"/g, '"');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get all markdown files in plan directory
|
|
84
|
+
function getPlanFiles(dir) {
|
|
85
|
+
const files = [];
|
|
86
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
if (item.isFile() && item.name.endsWith('.md')) {
|
|
90
|
+
files.push({
|
|
91
|
+
name: item.name,
|
|
92
|
+
path: path.join(dir, item.name),
|
|
93
|
+
isMain: item.name === 'plan.md',
|
|
94
|
+
isPhase: item.name.startsWith('phase-')
|
|
95
|
+
});
|
|
96
|
+
} else if (item.isDirectory()) {
|
|
97
|
+
// Include reports, research, scout subdirs
|
|
98
|
+
const subFiles = getPlanFiles(path.join(dir, item.name));
|
|
99
|
+
files.push(...subFiles.map(f => ({
|
|
100
|
+
...f,
|
|
101
|
+
name: `${item.name}/${f.name}`,
|
|
102
|
+
category: item.name
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files.sort((a, b) => {
|
|
108
|
+
if (a.isMain) return -1;
|
|
109
|
+
if (b.isMain) return 1;
|
|
110
|
+
if (a.isPhase && !b.isPhase) return -1;
|
|
111
|
+
if (!a.isPhase && b.isPhase) return 1;
|
|
112
|
+
return a.name.localeCompare(b.name);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generate HTML page
|
|
117
|
+
function generatePage(files, currentFile) {
|
|
118
|
+
const file = files.find(f => f.name === currentFile) || files[0];
|
|
119
|
+
const content = file ? fs.readFileSync(file.path, 'utf-8') : '# No plan found';
|
|
120
|
+
const htmlContent = markdownToHtml(content);
|
|
121
|
+
|
|
122
|
+
const nav = files.map(f => {
|
|
123
|
+
const isActive = f.name === (currentFile || files[0]?.name);
|
|
124
|
+
const icon = f.isMain ? 'š' : f.isPhase ? 'š' : f.category === 'research' ? 'š¬' : f.category === 'reports' ? 'š' : 'š';
|
|
125
|
+
return `<a href="?file=${encodeURIComponent(f.name)}" class="${isActive ? 'active' : ''}">${icon} ${f.name}</a>`;
|
|
126
|
+
}).join('\n');
|
|
127
|
+
|
|
128
|
+
return `<!DOCTYPE html>
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<head>
|
|
131
|
+
<meta charset="UTF-8">
|
|
132
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
133
|
+
<title>Plan Preview - ${path.basename(fullPlanPath)}</title>
|
|
134
|
+
<style>
|
|
135
|
+
:root {
|
|
136
|
+
--bg: #0d1117;
|
|
137
|
+
--bg-secondary: #161b22;
|
|
138
|
+
--text: #c9d1d9;
|
|
139
|
+
--text-muted: #8b949e;
|
|
140
|
+
--accent: #58a6ff;
|
|
141
|
+
--border: #30363d;
|
|
142
|
+
--code-bg: #1f2428;
|
|
143
|
+
--success: #3fb950;
|
|
144
|
+
--warning: #d29922;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
148
|
+
|
|
149
|
+
body {
|
|
150
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
151
|
+
background: var(--bg);
|
|
152
|
+
color: var(--text);
|
|
153
|
+
line-height: 1.6;
|
|
154
|
+
display: flex;
|
|
155
|
+
min-height: 100vh;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Sidebar */
|
|
159
|
+
.sidebar {
|
|
160
|
+
width: 280px;
|
|
161
|
+
background: var(--bg-secondary);
|
|
162
|
+
border-right: 1px solid var(--border);
|
|
163
|
+
padding: 20px;
|
|
164
|
+
position: fixed;
|
|
165
|
+
height: 100vh;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.sidebar h2 {
|
|
170
|
+
color: var(--text);
|
|
171
|
+
font-size: 14px;
|
|
172
|
+
text-transform: uppercase;
|
|
173
|
+
letter-spacing: 0.5px;
|
|
174
|
+
margin-bottom: 16px;
|
|
175
|
+
padding-bottom: 8px;
|
|
176
|
+
border-bottom: 1px solid var(--border);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.sidebar a {
|
|
180
|
+
display: block;
|
|
181
|
+
color: var(--text-muted);
|
|
182
|
+
text-decoration: none;
|
|
183
|
+
padding: 8px 12px;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
font-size: 13px;
|
|
186
|
+
margin-bottom: 4px;
|
|
187
|
+
transition: all 0.2s;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.sidebar a:hover {
|
|
191
|
+
background: var(--border);
|
|
192
|
+
color: var(--text);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.sidebar a.active {
|
|
196
|
+
background: rgba(88, 166, 255, 0.15);
|
|
197
|
+
color: var(--accent);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Main content */
|
|
201
|
+
.main {
|
|
202
|
+
flex: 1;
|
|
203
|
+
margin-left: 280px;
|
|
204
|
+
padding: 40px 60px;
|
|
205
|
+
max-width: 900px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.main h1 {
|
|
209
|
+
font-size: 32px;
|
|
210
|
+
margin-bottom: 24px;
|
|
211
|
+
padding-bottom: 16px;
|
|
212
|
+
border-bottom: 1px solid var(--border);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.main h2 {
|
|
216
|
+
font-size: 24px;
|
|
217
|
+
margin: 32px 0 16px;
|
|
218
|
+
color: var(--text);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.main h3 {
|
|
222
|
+
font-size: 18px;
|
|
223
|
+
margin: 24px 0 12px;
|
|
224
|
+
color: var(--text);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.main p {
|
|
228
|
+
margin-bottom: 16px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.main ul, .main ol {
|
|
232
|
+
margin: 16px 0;
|
|
233
|
+
padding-left: 24px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.main li {
|
|
237
|
+
margin-bottom: 8px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.main a {
|
|
241
|
+
color: var(--accent);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.main blockquote {
|
|
245
|
+
border-left: 4px solid var(--accent);
|
|
246
|
+
padding-left: 16px;
|
|
247
|
+
margin: 16px 0;
|
|
248
|
+
color: var(--text-muted);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.main hr {
|
|
252
|
+
border: none;
|
|
253
|
+
border-top: 1px solid var(--border);
|
|
254
|
+
margin: 32px 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Code */
|
|
258
|
+
pre {
|
|
259
|
+
background: var(--code-bg);
|
|
260
|
+
padding: 16px;
|
|
261
|
+
border-radius: 8px;
|
|
262
|
+
overflow-x: auto;
|
|
263
|
+
margin: 16px 0;
|
|
264
|
+
border: 1px solid var(--border);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
code {
|
|
268
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
269
|
+
font-size: 13px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
code.inline {
|
|
273
|
+
background: var(--code-bg);
|
|
274
|
+
padding: 2px 6px;
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
color: var(--accent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Tables */
|
|
280
|
+
table {
|
|
281
|
+
width: 100%;
|
|
282
|
+
border-collapse: collapse;
|
|
283
|
+
margin: 16px 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
th, td {
|
|
287
|
+
padding: 12px;
|
|
288
|
+
border: 1px solid var(--border);
|
|
289
|
+
text-align: left;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
th {
|
|
293
|
+
background: var(--bg-secondary);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Status badges */
|
|
297
|
+
.status-pending { color: var(--warning); }
|
|
298
|
+
.status-completed { color: var(--success); }
|
|
299
|
+
|
|
300
|
+
/* Footer */
|
|
301
|
+
.footer {
|
|
302
|
+
margin-top: 48px;
|
|
303
|
+
padding-top: 24px;
|
|
304
|
+
border-top: 1px solid var(--border);
|
|
305
|
+
color: var(--text-muted);
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Responsive */
|
|
310
|
+
@media (max-width: 768px) {
|
|
311
|
+
.sidebar { width: 100%; height: auto; position: relative; }
|
|
312
|
+
.main { margin-left: 0; padding: 20px; }
|
|
313
|
+
body { flex-direction: column; }
|
|
314
|
+
}
|
|
315
|
+
</style>
|
|
316
|
+
</head>
|
|
317
|
+
<body>
|
|
318
|
+
<nav class="sidebar">
|
|
319
|
+
<h2>š ${path.basename(fullPlanPath)}</h2>
|
|
320
|
+
${nav}
|
|
321
|
+
</nav>
|
|
322
|
+
<main class="main">
|
|
323
|
+
<article>
|
|
324
|
+
${htmlContent}
|
|
325
|
+
</article>
|
|
326
|
+
<footer class="footer">
|
|
327
|
+
<p>Plan Preview Server ⢠<a href="javascript:location.reload()">⻠Refresh</a></p>
|
|
328
|
+
</footer>
|
|
329
|
+
</main>
|
|
330
|
+
<script>
|
|
331
|
+
// Auto-refresh every 5 seconds (optional)
|
|
332
|
+
// setTimeout(() => location.reload(), 5000);
|
|
333
|
+
</script>
|
|
334
|
+
</body>
|
|
335
|
+
</html>`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create HTTP server
|
|
339
|
+
const server = http.createServer((req, res) => {
|
|
340
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
341
|
+
const currentFile = url.searchParams.get('file');
|
|
342
|
+
|
|
343
|
+
const files = getPlanFiles(fullPlanPath);
|
|
344
|
+
const html = generatePage(files, currentFile);
|
|
345
|
+
|
|
346
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
347
|
+
res.end(html);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
server.listen(PORT, () => {
|
|
351
|
+
const url = `http://localhost:${PORT}`;
|
|
352
|
+
console.log(`\nš Plan Preview Server`);
|
|
353
|
+
console.log(` Plan: ${planPath}`);
|
|
354
|
+
console.log(` URL: ${url}`);
|
|
355
|
+
console.log(`\n Press Ctrl+C to stop\n`);
|
|
356
|
+
|
|
357
|
+
// Open browser
|
|
358
|
+
const openCommand = process.platform === 'darwin' ? 'open' :
|
|
359
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
360
|
+
exec(`${openCommand} ${url}`);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Handle shutdown
|
|
364
|
+
process.on('SIGINT', () => {
|
|
365
|
+
console.log('\nš Server stopped');
|
|
366
|
+
process.exit(0);
|
|
367
|
+
});
|