claude-cli-advanced-starter-pack 1.8.4 → 1.8.5
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/cli/menu.js +171 -1
- package/src/cli/mobile-menu.js +230 -0
- package/src/commands/panel.js +84 -0
- package/src/data/releases.json +15 -0
- package/src/utils/happy-detect.js +66 -0
- package/templates/commands/menu-for-happy-ui.template.md +109 -0
- package/templates/hooks/github-progress-hook.template.cjs +248 -0
- package/templates/hooks/github-progress-hook.template.js +0 -197
package/package.json
CHANGED
package/src/cli/menu.js
CHANGED
|
@@ -20,11 +20,13 @@ import { runCreatePhaseDev, showPhasDevMainMenu } from '../commands/create-phase
|
|
|
20
20
|
import { runExploreMcp, showExploreMcpMenu } from '../commands/explore-mcp.js';
|
|
21
21
|
import { runClaudeAudit, showClaudeAuditMenu } from '../commands/claude-audit.js';
|
|
22
22
|
import { runRoadmap, showRoadmapMenu } from '../commands/roadmap.js';
|
|
23
|
-
import { launchPanel } from '../commands/panel.js';
|
|
23
|
+
import { launchPanel, launchPanelInline } from '../commands/panel.js';
|
|
24
24
|
import { hasTestingConfig } from '../testing/config.js';
|
|
25
25
|
import { showHelp } from '../commands/help.js';
|
|
26
26
|
import { hasValidConfig, getVersion, loadTechStack, saveTechStack } from '../utils.js';
|
|
27
27
|
import { performVersionCheck, formatUpdateBanner } from '../utils/version-check.js';
|
|
28
|
+
import { isHappyMode, shouldUseMobileUI } from '../utils/happy-detect.js';
|
|
29
|
+
import { showMobileMenu, showMobilePanel, showMobileSettings, mobileReturnPrompt } from './mobile-menu.js';
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Get bypass permissions status from settings.json
|
|
@@ -655,8 +657,15 @@ async function configureHappy(techStack) {
|
|
|
655
657
|
|
|
656
658
|
/**
|
|
657
659
|
* Show main interactive menu
|
|
660
|
+
* Automatically detects Happy CLI and switches to mobile-optimized UI
|
|
658
661
|
*/
|
|
659
662
|
export async function showMainMenu() {
|
|
663
|
+
// Check if we should use mobile UI (Happy CLI detected or happyMode.enabled)
|
|
664
|
+
const techStack = loadTechStack();
|
|
665
|
+
if (shouldUseMobileUI(techStack)) {
|
|
666
|
+
return showMobileMainMenu();
|
|
667
|
+
}
|
|
668
|
+
|
|
660
669
|
console.clear();
|
|
661
670
|
console.log(chalk.cyan(BANNER));
|
|
662
671
|
console.log('');
|
|
@@ -1063,3 +1072,164 @@ export function showWarning(message) {
|
|
|
1063
1072
|
export function showInfo(message) {
|
|
1064
1073
|
console.log(chalk.blue(`ℹ ${message}`));
|
|
1065
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Mobile-optimized main menu handler
|
|
1078
|
+
* Routes actions from the mobile menu to their handlers
|
|
1079
|
+
*/
|
|
1080
|
+
async function showMobileMainMenu() {
|
|
1081
|
+
const action = await showMobileMenu();
|
|
1082
|
+
|
|
1083
|
+
switch (action) {
|
|
1084
|
+
case 'create':
|
|
1085
|
+
const configured = hasValidConfig();
|
|
1086
|
+
if (!configured) {
|
|
1087
|
+
console.log(chalk.yellow('Setup required first.'));
|
|
1088
|
+
const { proceed } = await inquirer.prompt([
|
|
1089
|
+
{ type: 'confirm', name: 'proceed', message: 'Run setup?', default: true }
|
|
1090
|
+
]);
|
|
1091
|
+
if (proceed) await runSetup({});
|
|
1092
|
+
} else {
|
|
1093
|
+
await runCreate({});
|
|
1094
|
+
}
|
|
1095
|
+
break;
|
|
1096
|
+
|
|
1097
|
+
case 'decompose':
|
|
1098
|
+
if (!hasValidConfig()) {
|
|
1099
|
+
console.log(chalk.yellow('Setup required first.'));
|
|
1100
|
+
} else {
|
|
1101
|
+
await runDecompose({});
|
|
1102
|
+
}
|
|
1103
|
+
break;
|
|
1104
|
+
|
|
1105
|
+
case 'sync':
|
|
1106
|
+
if (!hasValidConfig()) {
|
|
1107
|
+
console.log(chalk.yellow('Setup required first.'));
|
|
1108
|
+
} else {
|
|
1109
|
+
await runSync({ subcommand: 'status' });
|
|
1110
|
+
}
|
|
1111
|
+
break;
|
|
1112
|
+
|
|
1113
|
+
case 'setup':
|
|
1114
|
+
await runSetup({});
|
|
1115
|
+
break;
|
|
1116
|
+
|
|
1117
|
+
case 'list':
|
|
1118
|
+
if (!hasValidConfig()) {
|
|
1119
|
+
console.log(chalk.yellow('Setup required first.'));
|
|
1120
|
+
} else {
|
|
1121
|
+
await runList({});
|
|
1122
|
+
}
|
|
1123
|
+
break;
|
|
1124
|
+
|
|
1125
|
+
case 'install':
|
|
1126
|
+
await runInstall({});
|
|
1127
|
+
break;
|
|
1128
|
+
|
|
1129
|
+
case 'panel-inline':
|
|
1130
|
+
// Show panel inline instead of launching new window
|
|
1131
|
+
await showMobilePanelLoop();
|
|
1132
|
+
break;
|
|
1133
|
+
|
|
1134
|
+
case 'test-setup':
|
|
1135
|
+
await runTestSetup({});
|
|
1136
|
+
break;
|
|
1137
|
+
|
|
1138
|
+
case 'agent-creator':
|
|
1139
|
+
await runCreateAgent({});
|
|
1140
|
+
break;
|
|
1141
|
+
|
|
1142
|
+
case 'explore-mcp':
|
|
1143
|
+
await showExploreMcpMenu();
|
|
1144
|
+
break;
|
|
1145
|
+
|
|
1146
|
+
case 'project-settings':
|
|
1147
|
+
await showMobileSettingsLoop();
|
|
1148
|
+
break;
|
|
1149
|
+
|
|
1150
|
+
case 'help':
|
|
1151
|
+
showHelp();
|
|
1152
|
+
break;
|
|
1153
|
+
|
|
1154
|
+
case 'exit':
|
|
1155
|
+
console.log(chalk.dim('Goodbye!'));
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Return to menu unless exiting
|
|
1160
|
+
if (action !== 'exit') {
|
|
1161
|
+
const back = await mobileReturnPrompt();
|
|
1162
|
+
if (back) {
|
|
1163
|
+
await showMobileMainMenu();
|
|
1164
|
+
} else {
|
|
1165
|
+
console.log(chalk.dim('Goodbye!'));
|
|
1166
|
+
process.exit(0);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Mobile panel loop - inline panel without new window
|
|
1173
|
+
*/
|
|
1174
|
+
async function showMobilePanelLoop() {
|
|
1175
|
+
while (true) {
|
|
1176
|
+
const action = await showMobilePanel();
|
|
1177
|
+
|
|
1178
|
+
if (action === 'back') {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Copy command to clipboard and show instructions
|
|
1183
|
+
console.log('');
|
|
1184
|
+
console.log(chalk.cyan(`Command: ${action}`));
|
|
1185
|
+
console.log(chalk.dim('Paste in Claude Code'));
|
|
1186
|
+
console.log('');
|
|
1187
|
+
|
|
1188
|
+
// Try to copy to clipboard
|
|
1189
|
+
try {
|
|
1190
|
+
const { copyToClipboard } = await import('../panel/queue.js');
|
|
1191
|
+
if (copyToClipboard(action)) {
|
|
1192
|
+
console.log(chalk.green('✓ Copied to clipboard'));
|
|
1193
|
+
}
|
|
1194
|
+
} catch {
|
|
1195
|
+
// Clipboard not available
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
await inquirer.prompt([
|
|
1199
|
+
{ type: 'input', name: 'continue', message: 'Enter to continue...' }
|
|
1200
|
+
]);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Mobile settings loop
|
|
1206
|
+
*/
|
|
1207
|
+
async function showMobileSettingsLoop() {
|
|
1208
|
+
while (true) {
|
|
1209
|
+
const action = await showMobileSettings();
|
|
1210
|
+
|
|
1211
|
+
if (action === 'back') {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const techStack = loadTechStack();
|
|
1216
|
+
|
|
1217
|
+
switch (action) {
|
|
1218
|
+
case 'github':
|
|
1219
|
+
await configureGitHub(techStack);
|
|
1220
|
+
break;
|
|
1221
|
+
case 'deployment':
|
|
1222
|
+
await configureDeployment(techStack);
|
|
1223
|
+
break;
|
|
1224
|
+
case 'tunnel':
|
|
1225
|
+
await configureTunnel(techStack);
|
|
1226
|
+
break;
|
|
1227
|
+
case 'token':
|
|
1228
|
+
await configureToken(techStack);
|
|
1229
|
+
break;
|
|
1230
|
+
case 'happy':
|
|
1231
|
+
await configureHappy(techStack);
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-Optimized Menu System for Happy CLI
|
|
3
|
+
*
|
|
4
|
+
* Renders menus optimized for mobile screens (max 40 chars).
|
|
5
|
+
* Single-column layout, no overflow, minimal decorations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import { getVersion, loadTechStack, hasValidConfig } from '../utils.js';
|
|
11
|
+
import { isHappyMode, getMobileWidth } from '../utils/happy-detect.js';
|
|
12
|
+
|
|
13
|
+
// Mobile-friendly banner (40 chars max)
|
|
14
|
+
const MOBILE_BANNER = `
|
|
15
|
+
${chalk.cyan('╔══════════════════════════════════╗')}
|
|
16
|
+
${chalk.cyan('║')} ${chalk.bold('CCASP')} ${chalk.dim('v' + getVersion().slice(0, 5))}${' '.repeat(17)}${chalk.cyan('║')}
|
|
17
|
+
${chalk.cyan('║')} ${chalk.dim('Mobile Menu')}${' '.repeat(21)}${chalk.cyan('║')}
|
|
18
|
+
${chalk.cyan('╚══════════════════════════════════╝')}`;
|
|
19
|
+
|
|
20
|
+
// Compact panel banner for mobile
|
|
21
|
+
const MOBILE_PANEL_BANNER = `
|
|
22
|
+
${chalk.cyan('╔══════════════════════════════════╗')}
|
|
23
|
+
${chalk.cyan('║')} ${chalk.bold('CCASP Panel')}${' '.repeat(20)}${chalk.cyan('║')}
|
|
24
|
+
${chalk.cyan('╚══════════════════════════════════╝')}`;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Truncate text to fit mobile width
|
|
28
|
+
* @param {string} text - Text to truncate
|
|
29
|
+
* @param {number} maxLen - Maximum length
|
|
30
|
+
* @returns {string} Truncated text
|
|
31
|
+
*/
|
|
32
|
+
function truncate(text, maxLen = 30) {
|
|
33
|
+
if (!text) return '';
|
|
34
|
+
// Strip ANSI codes for length calculation
|
|
35
|
+
const plainText = text.replace(/\x1B\[[0-9;]*m/g, '');
|
|
36
|
+
if (plainText.length <= maxLen) return text;
|
|
37
|
+
return text.slice(0, maxLen - 2) + '..';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format a menu item for mobile (single line)
|
|
42
|
+
* @param {string} key - Shortcut key
|
|
43
|
+
* @param {string} label - Item label
|
|
44
|
+
* @returns {string} Formatted menu item
|
|
45
|
+
*/
|
|
46
|
+
function formatMobileItem(key, label) {
|
|
47
|
+
const truncLabel = truncate(label, 28);
|
|
48
|
+
return `${chalk.yellow(key + ')')} ${truncLabel}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Show mobile-optimized main menu
|
|
53
|
+
*/
|
|
54
|
+
export async function showMobileMenu() {
|
|
55
|
+
console.clear();
|
|
56
|
+
console.log(MOBILE_BANNER);
|
|
57
|
+
|
|
58
|
+
const configured = hasValidConfig();
|
|
59
|
+
|
|
60
|
+
// Status line
|
|
61
|
+
if (configured) {
|
|
62
|
+
console.log(chalk.green(' ✓ Configured'));
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.yellow(' ⚠ Not configured'));
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
// Single-column menu items
|
|
69
|
+
const choices = [
|
|
70
|
+
{ name: formatMobileItem('1', 'Create Task'), value: 'create' },
|
|
71
|
+
{ name: formatMobileItem('2', 'Decompose Issue'), value: 'decompose' },
|
|
72
|
+
{ name: formatMobileItem('3', 'Sync Tasks'), value: 'sync' },
|
|
73
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
74
|
+
{ name: formatMobileItem('4', 'Setup'), value: 'setup' },
|
|
75
|
+
{ name: formatMobileItem('5', 'List Tasks'), value: 'list' },
|
|
76
|
+
{ name: formatMobileItem('6', 'Install Command'), value: 'install' },
|
|
77
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
78
|
+
{ name: formatMobileItem('P', 'Panel (inline)'), value: 'panel-inline' },
|
|
79
|
+
{ name: formatMobileItem('T', 'Test Setup'), value: 'test-setup' },
|
|
80
|
+
{ name: formatMobileItem('A', 'Agent Creator'), value: 'agent-creator' },
|
|
81
|
+
{ name: formatMobileItem('M', 'MCP Explorer'), value: 'explore-mcp' },
|
|
82
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
83
|
+
{ name: formatMobileItem('S', 'Settings'), value: 'project-settings' },
|
|
84
|
+
{ name: formatMobileItem('?', 'Help'), value: 'help' },
|
|
85
|
+
{ name: formatMobileItem('Q', 'Exit'), value: 'exit' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const { action } = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: 'list',
|
|
91
|
+
name: 'action',
|
|
92
|
+
message: 'Select:',
|
|
93
|
+
choices,
|
|
94
|
+
pageSize: 12,
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
return action;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Show mobile-optimized panel menu (inline, no new window)
|
|
103
|
+
*/
|
|
104
|
+
export async function showMobilePanel() {
|
|
105
|
+
console.clear();
|
|
106
|
+
console.log(MOBILE_PANEL_BANNER);
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
// Single-column panel items
|
|
110
|
+
const choices = [
|
|
111
|
+
new inquirer.Separator(chalk.cyan(' Agents & Skills')),
|
|
112
|
+
{ name: formatMobileItem('A', 'Create Agent'), value: '/create-agent' },
|
|
113
|
+
{ name: formatMobileItem('H', 'Create Hook'), value: '/create-hook' },
|
|
114
|
+
{ name: formatMobileItem('S', 'Create Skill'), value: '/create-skill' },
|
|
115
|
+
{ name: formatMobileItem('M', 'Explore MCP'), value: '/explore-mcp' },
|
|
116
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
117
|
+
new inquirer.Separator(chalk.cyan(' Resources')),
|
|
118
|
+
{ name: formatMobileItem('1', 'View Agents'), value: '/view-agents' },
|
|
119
|
+
{ name: formatMobileItem('2', 'View Skills'), value: '/view-skills' },
|
|
120
|
+
{ name: formatMobileItem('3', 'View Hooks'), value: '/view-hooks' },
|
|
121
|
+
{ name: formatMobileItem('4', 'All Commands'), value: '/INDEX' },
|
|
122
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
123
|
+
new inquirer.Separator(chalk.cyan(' Quick Actions')),
|
|
124
|
+
{ name: formatMobileItem('P', 'Phase Dev Plan'), value: '/phase-dev-plan' },
|
|
125
|
+
{ name: formatMobileItem('G', 'GitHub Task'), value: '/github-task' },
|
|
126
|
+
{ name: formatMobileItem('T', 'Run E2E Tests'), value: '/e2e-test' },
|
|
127
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
128
|
+
{ name: formatMobileItem('B', 'Back'), value: 'back' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const { action } = await inquirer.prompt([
|
|
132
|
+
{
|
|
133
|
+
type: 'list',
|
|
134
|
+
name: 'action',
|
|
135
|
+
message: 'Select:',
|
|
136
|
+
choices,
|
|
137
|
+
pageSize: 15,
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
return action;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Show mobile-optimized project settings
|
|
146
|
+
*/
|
|
147
|
+
export async function showMobileSettings() {
|
|
148
|
+
console.clear();
|
|
149
|
+
console.log(chalk.cyan('╔══════════════════════════════════╗'));
|
|
150
|
+
console.log(chalk.cyan('║') + chalk.bold(' Settings') + ' '.repeat(23) + chalk.cyan('║'));
|
|
151
|
+
console.log(chalk.cyan('╚══════════════════════════════════╝'));
|
|
152
|
+
console.log('');
|
|
153
|
+
|
|
154
|
+
const choices = [
|
|
155
|
+
{ name: formatMobileItem('1', 'GitHub Board'), value: 'github' },
|
|
156
|
+
{ name: formatMobileItem('2', 'Deployment'), value: 'deployment' },
|
|
157
|
+
{ name: formatMobileItem('3', 'Tunnel'), value: 'tunnel' },
|
|
158
|
+
{ name: formatMobileItem('4', 'Token Budget'), value: 'token' },
|
|
159
|
+
{ name: formatMobileItem('5', 'Happy Mode'), value: 'happy' },
|
|
160
|
+
new inquirer.Separator(chalk.dim('─'.repeat(34))),
|
|
161
|
+
{ name: formatMobileItem('B', 'Back'), value: 'back' },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const { action } = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'list',
|
|
167
|
+
name: 'action',
|
|
168
|
+
message: 'Select:',
|
|
169
|
+
choices,
|
|
170
|
+
pageSize: 10,
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
return action;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Show a mobile-friendly success message
|
|
179
|
+
* @param {string} message - Success message
|
|
180
|
+
*/
|
|
181
|
+
export function showMobileSuccess(message) {
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.green(`✓ ${truncate(message, 32)}`));
|
|
184
|
+
console.log('');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Show a mobile-friendly error message
|
|
189
|
+
* @param {string} message - Error message
|
|
190
|
+
*/
|
|
191
|
+
export function showMobileError(message) {
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(chalk.red(`✗ ${truncate(message, 32)}`));
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Show a mobile-friendly info box
|
|
199
|
+
* @param {string} title - Box title
|
|
200
|
+
* @param {string[]} lines - Content lines
|
|
201
|
+
*/
|
|
202
|
+
export function showMobileBox(title, lines = []) {
|
|
203
|
+
const width = getMobileWidth() - 4; // Account for borders
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.cyan('┌' + '─'.repeat(width) + '┐'));
|
|
206
|
+
console.log(chalk.cyan('│') + ` ${truncate(title, width - 2)}`.padEnd(width) + chalk.cyan('│'));
|
|
207
|
+
console.log(chalk.cyan('├' + '─'.repeat(width) + '┤'));
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
console.log(chalk.cyan('│') + ` ${truncate(line, width - 2)}`.padEnd(width) + chalk.cyan('│'));
|
|
210
|
+
}
|
|
211
|
+
console.log(chalk.cyan('└' + '─'.repeat(width) + '┘'));
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Prompt to return to menu (mobile version)
|
|
217
|
+
* @returns {Promise<boolean>} True if user wants to return
|
|
218
|
+
*/
|
|
219
|
+
export async function mobileReturnPrompt() {
|
|
220
|
+
console.log('');
|
|
221
|
+
const { back } = await inquirer.prompt([
|
|
222
|
+
{
|
|
223
|
+
type: 'confirm',
|
|
224
|
+
name: 'back',
|
|
225
|
+
message: 'Back to menu?',
|
|
226
|
+
default: true,
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
return back;
|
|
230
|
+
}
|
package/src/commands/panel.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
ensureQueueDir,
|
|
21
21
|
} from '../panel/queue.js';
|
|
22
22
|
import { getVersion } from '../utils.js';
|
|
23
|
+
import { isHappyMode } from '../utils/happy-detect.js';
|
|
23
24
|
|
|
24
25
|
// Panel ASCII Banner
|
|
25
26
|
const PANEL_BANNER = `
|
|
@@ -249,8 +250,15 @@ export async function runPanel(options = {}) {
|
|
|
249
250
|
|
|
250
251
|
/**
|
|
251
252
|
* Launch panel in a new terminal window
|
|
253
|
+
* Automatically detects Happy CLI and falls back to inline mode
|
|
252
254
|
*/
|
|
253
255
|
export async function launchPanel() {
|
|
256
|
+
// If Happy mode detected, use inline panel instead of new window
|
|
257
|
+
if (isHappyMode()) {
|
|
258
|
+
console.log(chalk.cyan('\n Happy CLI detected - using inline panel\n'));
|
|
259
|
+
return launchPanelInline();
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
console.log(chalk.cyan('\n Launching CCASP Panel in new window...\n'));
|
|
255
263
|
|
|
256
264
|
const ccaspPath = join(process.cwd(), 'node_modules', '.bin', 'ccasp');
|
|
@@ -295,3 +303,79 @@ export async function launchPanel() {
|
|
|
295
303
|
console.log(chalk.dim(` Run manually: ${chalk.white('ccasp panel')}\n`));
|
|
296
304
|
}
|
|
297
305
|
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Launch panel inline (mobile-friendly, no new window)
|
|
309
|
+
* Used when Happy CLI is detected or explicitly requested
|
|
310
|
+
*/
|
|
311
|
+
export async function launchPanelInline() {
|
|
312
|
+
const MOBILE_PANEL_BANNER = `
|
|
313
|
+
${chalk.cyan('╔══════════════════════════════════╗')}
|
|
314
|
+
${chalk.cyan('║')} ${chalk.bold('Panel')} ${chalk.dim('(inline)')}${' '.repeat(17)}${chalk.cyan('║')}
|
|
315
|
+
${chalk.cyan('╚══════════════════════════════════╝')}`;
|
|
316
|
+
|
|
317
|
+
console.clear();
|
|
318
|
+
console.log(MOBILE_PANEL_BANNER);
|
|
319
|
+
console.log('');
|
|
320
|
+
|
|
321
|
+
// Simplified single-column panel for mobile
|
|
322
|
+
const sections = [
|
|
323
|
+
{ header: 'Agents', items: [
|
|
324
|
+
{ key: 'A', label: 'Create Agent', cmd: '/create-agent' },
|
|
325
|
+
{ key: 'H', label: 'Create Hook', cmd: '/create-hook' },
|
|
326
|
+
{ key: 'S', label: 'Create Skill', cmd: '/create-skill' },
|
|
327
|
+
{ key: 'M', label: 'Explore MCP', cmd: '/explore-mcp' },
|
|
328
|
+
]},
|
|
329
|
+
{ header: 'Actions', items: [
|
|
330
|
+
{ key: 'P', label: 'Phase Dev', cmd: '/phase-dev-plan' },
|
|
331
|
+
{ key: 'G', label: 'GitHub Task', cmd: '/github-task' },
|
|
332
|
+
{ key: 'T', label: 'E2E Tests', cmd: '/e2e-test' },
|
|
333
|
+
]},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
// Build choices
|
|
337
|
+
const choices = [];
|
|
338
|
+
for (const section of sections) {
|
|
339
|
+
choices.push(new inquirer.Separator(chalk.cyan(` ${section.header}`)));
|
|
340
|
+
for (const item of section.items) {
|
|
341
|
+
choices.push({
|
|
342
|
+
name: `${chalk.yellow(item.key + ')')} ${item.label}`,
|
|
343
|
+
value: item.cmd,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
choices.push(new inquirer.Separator(chalk.dim('─'.repeat(34))));
|
|
348
|
+
choices.push({ name: `${chalk.yellow('B)')} Back`, value: 'back' });
|
|
349
|
+
|
|
350
|
+
const { action } = await inquirer.prompt([{
|
|
351
|
+
type: 'list',
|
|
352
|
+
name: 'action',
|
|
353
|
+
message: 'Select:',
|
|
354
|
+
choices,
|
|
355
|
+
pageSize: 12,
|
|
356
|
+
}]);
|
|
357
|
+
|
|
358
|
+
if (action === 'back') {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Show command and copy to clipboard
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(chalk.cyan(`Command: ${action}`));
|
|
365
|
+
|
|
366
|
+
const copied = copyToClipboard(action);
|
|
367
|
+
if (copied) {
|
|
368
|
+
console.log(chalk.green('✓ Copied to clipboard'));
|
|
369
|
+
}
|
|
370
|
+
console.log(chalk.dim('Paste in Claude Code'));
|
|
371
|
+
console.log('');
|
|
372
|
+
|
|
373
|
+
await inquirer.prompt([{
|
|
374
|
+
type: 'input',
|
|
375
|
+
name: 'continue',
|
|
376
|
+
message: 'Enter to continue...',
|
|
377
|
+
}]);
|
|
378
|
+
|
|
379
|
+
// Loop back to panel
|
|
380
|
+
return launchPanelInline();
|
|
381
|
+
}
|
package/src/data/releases.json
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"releases": [
|
|
3
|
+
{
|
|
4
|
+
"version": "1.8.5",
|
|
5
|
+
"date": "2026-01-31",
|
|
6
|
+
"summary": "Release notes pending",
|
|
7
|
+
"highlights": [],
|
|
8
|
+
"newFeatures": {
|
|
9
|
+
"commands": [],
|
|
10
|
+
"agents": [],
|
|
11
|
+
"skills": [],
|
|
12
|
+
"hooks": [],
|
|
13
|
+
"other": []
|
|
14
|
+
},
|
|
15
|
+
"breaking": [],
|
|
16
|
+
"deprecated": []
|
|
17
|
+
},
|
|
3
18
|
{
|
|
4
19
|
"version": "1.8.4",
|
|
5
20
|
"date": "2026-01-31",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Happy CLI Detection Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects if CCASP is running inside Happy Coder mobile CLI wrapper.
|
|
5
|
+
* Happy CLI sets HAPPY_* environment variables when active.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/slopus/happy-cli
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if running inside Happy CLI environment
|
|
12
|
+
* @returns {boolean} True if Happy CLI detected
|
|
13
|
+
*/
|
|
14
|
+
export function isHappyMode() {
|
|
15
|
+
return !!(
|
|
16
|
+
process.env.HAPPY_HOME_DIR ||
|
|
17
|
+
process.env.HAPPY_SERVER_URL ||
|
|
18
|
+
process.env.HAPPY_WEBAPP_URL ||
|
|
19
|
+
process.env.HAPPY_EXPERIMENTAL
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get Happy CLI configuration from environment
|
|
25
|
+
* @returns {object} Happy configuration details
|
|
26
|
+
*/
|
|
27
|
+
export function getHappyConfig() {
|
|
28
|
+
return {
|
|
29
|
+
detected: isHappyMode(),
|
|
30
|
+
homeDir: process.env.HAPPY_HOME_DIR || null,
|
|
31
|
+
serverUrl: process.env.HAPPY_SERVER_URL || 'https://api.cluster-fluster.com',
|
|
32
|
+
webappUrl: process.env.HAPPY_WEBAPP_URL || 'https://app.happy.engineering',
|
|
33
|
+
experimental: process.env.HAPPY_EXPERIMENTAL === 'true',
|
|
34
|
+
disableCaffeinate: process.env.HAPPY_DISABLE_CAFFEINATE === 'true',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get recommended terminal width for mobile display
|
|
40
|
+
* @returns {number} Optimal width for mobile screens
|
|
41
|
+
*/
|
|
42
|
+
export function getMobileWidth() {
|
|
43
|
+
// Happy mobile UI typically works best at 40 chars
|
|
44
|
+
// to avoid horizontal scrolling on phone screens
|
|
45
|
+
return 40;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if we should use mobile-optimized UI
|
|
50
|
+
* Respects both Happy CLI detection and tech-stack.json happyMode.enabled
|
|
51
|
+
* @param {object} techStack - Optional tech-stack.json config
|
|
52
|
+
* @returns {boolean} True if mobile UI should be used
|
|
53
|
+
*/
|
|
54
|
+
export function shouldUseMobileUI(techStack = null) {
|
|
55
|
+
// Auto-detect Happy CLI environment
|
|
56
|
+
if (isHappyMode()) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check tech-stack.json happyMode.enabled setting
|
|
61
|
+
if (techStack?.happyMode?.enabled) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Mobile-friendly menu optimized for Happy.Engineering CLI (no overflow)
|
|
3
|
+
model: sonnet
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /menu-for-happy-ui - Mobile-Optimized CCASP Menu
|
|
7
|
+
|
|
8
|
+
Display the CCASP menu in a mobile-friendly format that doesn't overflow on small screens.
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
This command is a fallback for when automatic Happy CLI detection fails. It forces the mobile-optimized menu layout regardless of environment.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Running CCASP through Happy Coder mobile app
|
|
17
|
+
- Using a narrow terminal window
|
|
18
|
+
- Automatic detection (`HAPPY_*` env vars) not working
|
|
19
|
+
- Preference for compact menu layout
|
|
20
|
+
|
|
21
|
+
## Mobile Menu Features
|
|
22
|
+
|
|
23
|
+
- **Max 40 character width** - No horizontal scrolling
|
|
24
|
+
- **Single-column layout** - Easy vertical scrolling
|
|
25
|
+
- **Minimal decorations** - Less visual noise
|
|
26
|
+
- **Inline panel** - No new window launch (works with Happy)
|
|
27
|
+
|
|
28
|
+
## Menu Layout
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
╔══════════════════════════════════╗
|
|
32
|
+
║ CCASP v1.x ║
|
|
33
|
+
║ Mobile Menu ║
|
|
34
|
+
╚══════════════════════════════════╝
|
|
35
|
+
✓ Configured
|
|
36
|
+
|
|
37
|
+
1) Create Task
|
|
38
|
+
2) Decompose Issue
|
|
39
|
+
3) Sync Tasks
|
|
40
|
+
──────────────────────────────────
|
|
41
|
+
4) Setup
|
|
42
|
+
5) List Tasks
|
|
43
|
+
6) Install Command
|
|
44
|
+
──────────────────────────────────
|
|
45
|
+
P) Panel (inline)
|
|
46
|
+
T) Test Setup
|
|
47
|
+
A) Agent Creator
|
|
48
|
+
M) MCP Explorer
|
|
49
|
+
──────────────────────────────────
|
|
50
|
+
S) Settings
|
|
51
|
+
?) Help
|
|
52
|
+
Q) Exit
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Instructions for Claude
|
|
56
|
+
|
|
57
|
+
When this command is invoked:
|
|
58
|
+
|
|
59
|
+
1. **Display Mobile Banner**
|
|
60
|
+
- Use 36-character width box
|
|
61
|
+
- Show version number (truncated)
|
|
62
|
+
- Show configuration status
|
|
63
|
+
|
|
64
|
+
2. **Present Menu Options**
|
|
65
|
+
- Single column with numbered shortcuts
|
|
66
|
+
- Separator lines using `─` (34 chars)
|
|
67
|
+
- No descriptions (too wide for mobile)
|
|
68
|
+
|
|
69
|
+
3. **Handle Selection**
|
|
70
|
+
- Route to appropriate handler
|
|
71
|
+
- For panel: display inline (no new window)
|
|
72
|
+
- Return to menu after each action
|
|
73
|
+
|
|
74
|
+
4. **Settings Submenu**
|
|
75
|
+
- Same compact format
|
|
76
|
+
- Quick access to: GitHub, Deployment, Tunnel, Token, Happy
|
|
77
|
+
|
|
78
|
+
## Comparison with Standard Menu
|
|
79
|
+
|
|
80
|
+
| Feature | Standard | Mobile |
|
|
81
|
+
|---------|----------|--------|
|
|
82
|
+
| Width | 76 chars | 36 chars |
|
|
83
|
+
| Columns | Multi | Single |
|
|
84
|
+
| Descriptions | Full | None |
|
|
85
|
+
| Panel launch | New window | Inline |
|
|
86
|
+
| ASCII art | Full banner | Minimal |
|
|
87
|
+
|
|
88
|
+
## Automatic Detection
|
|
89
|
+
|
|
90
|
+
CCASP automatically detects Happy CLI via environment variables:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// Auto-detected when any of these are set:
|
|
94
|
+
process.env.HAPPY_HOME_DIR
|
|
95
|
+
process.env.HAPPY_SERVER_URL
|
|
96
|
+
process.env.HAPPY_WEBAPP_URL
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If these are set, `/menu` automatically uses mobile layout. Use `/menu-for-happy-ui` only when auto-detection fails.
|
|
100
|
+
|
|
101
|
+
## Related Commands
|
|
102
|
+
|
|
103
|
+
- `/menu` - Standard menu (auto-detects Happy)
|
|
104
|
+
- `/happy-start` - Initialize Happy Mode session
|
|
105
|
+
- `/ccasp-panel` - Standard panel (new window)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
*For Happy.Engineering mobile CLI users*
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Progress Hook
|
|
3
|
+
*
|
|
4
|
+
* Automatically updates GitHub issues as tasks are completed.
|
|
5
|
+
* Monitors TodoWrite calls and syncs progress to linked GitHub issues.
|
|
6
|
+
*
|
|
7
|
+
* Event: PostToolUse
|
|
8
|
+
* Matcher: TodoWrite
|
|
9
|
+
*
|
|
10
|
+
* This hook reads from stdin (Claude Code passes tool info there)
|
|
11
|
+
* and updates the linked GitHub issue with progress comments.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Configuration - parse from tech-stack.json dynamically
|
|
19
|
+
function loadConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const techStackPath = path.join(process.cwd(), '.claude', 'config', 'tech-stack.json');
|
|
22
|
+
if (fs.existsSync(techStackPath)) {
|
|
23
|
+
const techStack = JSON.parse(fs.readFileSync(techStackPath, 'utf8'));
|
|
24
|
+
const vc = techStack.versionControl || {};
|
|
25
|
+
const repo = vc.repository || '';
|
|
26
|
+
const [owner, repoName] = repo.includes('/') ? repo.split('/') : ['', ''];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
owner: owner,
|
|
30
|
+
repo: repoName,
|
|
31
|
+
projectNumber: vc.projectBoard?.number || null,
|
|
32
|
+
enabled: !!vc.projectBoard?.type && !!owner && !!repoName,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Silent fail - config not available
|
|
37
|
+
}
|
|
38
|
+
return { owner: '', repo: '', projectNumber: null, enabled: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PROGRESS_FILE = '.claude/hooks/cache/github-progress.json';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load progress tracking data
|
|
45
|
+
*/
|
|
46
|
+
function loadProgress() {
|
|
47
|
+
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(progressPath)) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(progressPath, 'utf8'));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Could not parse, return default
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
linkedIssue: null,
|
|
59
|
+
tasks: [],
|
|
60
|
+
completedTasks: [],
|
|
61
|
+
lastUpdate: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Save progress tracking data
|
|
67
|
+
*/
|
|
68
|
+
function saveProgress(progress) {
|
|
69
|
+
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
70
|
+
const progressDir = path.dirname(progressPath);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(progressDir)) {
|
|
73
|
+
fs.mkdirSync(progressDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
progress.lastUpdate = new Date().toISOString();
|
|
77
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if gh CLI is available
|
|
82
|
+
*/
|
|
83
|
+
function hasGhCli() {
|
|
84
|
+
try {
|
|
85
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Update GitHub issue with progress comment
|
|
94
|
+
*/
|
|
95
|
+
function updateGitHubIssue(issueNumber, completedCount, totalCount, latestTask, config) {
|
|
96
|
+
if (!hasGhCli()) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Create progress comment
|
|
102
|
+
const percentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
|
|
103
|
+
const progressBar = '█'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10));
|
|
104
|
+
|
|
105
|
+
const comment = `### Progress Update
|
|
106
|
+
|
|
107
|
+
${progressBar} ${percentage}% (${completedCount}/${totalCount} tasks)
|
|
108
|
+
|
|
109
|
+
**Latest completed:** ${latestTask || 'N/A'}
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
*Auto-updated by Claude Code github-progress-hook*`;
|
|
113
|
+
|
|
114
|
+
// Add comment to issue - use file to avoid escaping issues
|
|
115
|
+
const cacheDir = path.join(process.cwd(), '.claude', 'hooks', 'cache');
|
|
116
|
+
if (!fs.existsSync(cacheDir)) {
|
|
117
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
const tmpFile = path.join(cacheDir, 'tmp-comment.md');
|
|
120
|
+
fs.writeFileSync(tmpFile, comment, 'utf8');
|
|
121
|
+
|
|
122
|
+
execSync(
|
|
123
|
+
`gh issue comment ${issueNumber} --repo ${config.owner}/${config.repo} --body-file "${tmpFile}"`,
|
|
124
|
+
{ stdio: 'pipe' }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Clean up temp file
|
|
128
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
129
|
+
|
|
130
|
+
console.log(`[github-progress] Updated issue #${issueNumber}: ${percentage}% complete`);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// Silent fail
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract linked issue number from todos
|
|
140
|
+
*/
|
|
141
|
+
function findLinkedIssue(todos, progress) {
|
|
142
|
+
// Check if issue is already linked
|
|
143
|
+
if (progress.linkedIssue) {
|
|
144
|
+
return progress.linkedIssue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Look for issue reference in todo content (e.g., "Issue #11" or "#11")
|
|
148
|
+
for (const todo of todos) {
|
|
149
|
+
const content = todo.content || '';
|
|
150
|
+
const issueMatch = content.match(/(?:Issue\s*)?#(\d+)/i);
|
|
151
|
+
if (issueMatch) {
|
|
152
|
+
return parseInt(issueMatch[1], 10);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Main function - reads from stdin and processes TodoWrite
|
|
161
|
+
*/
|
|
162
|
+
async function main() {
|
|
163
|
+
const config = loadConfig();
|
|
164
|
+
|
|
165
|
+
// Skip if not configured
|
|
166
|
+
if (!config.enabled) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Read stdin to get tool information
|
|
171
|
+
let input = '';
|
|
172
|
+
try {
|
|
173
|
+
input = fs.readFileSync(0, 'utf8');
|
|
174
|
+
} catch {
|
|
175
|
+
// No stdin available
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let toolData;
|
|
180
|
+
try {
|
|
181
|
+
toolData = JSON.parse(input);
|
|
182
|
+
} catch {
|
|
183
|
+
// Invalid JSON
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check if this is a TodoWrite tool
|
|
188
|
+
const toolName = toolData.tool_name || toolData.name;
|
|
189
|
+
if (toolName !== 'TodoWrite') {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get todos from input
|
|
194
|
+
const todos = toolData.tool_input?.todos || toolData.input?.todos || [];
|
|
195
|
+
if (todos.length === 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Load progress tracking
|
|
200
|
+
const progress = loadProgress();
|
|
201
|
+
|
|
202
|
+
// Check for linked issue
|
|
203
|
+
const issueNumber = findLinkedIssue(todos, progress);
|
|
204
|
+
|
|
205
|
+
if (issueNumber && !progress.linkedIssue) {
|
|
206
|
+
progress.linkedIssue = issueNumber;
|
|
207
|
+
console.log(`[github-progress] Linked to issue #${issueNumber}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Skip if no linked issue
|
|
211
|
+
if (!progress.linkedIssue) {
|
|
212
|
+
saveProgress(progress);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Count completed vs total tasks (excluding CONTEXT task)
|
|
217
|
+
const actualTodos = todos.filter(t => !t.content?.startsWith('CONTEXT:'));
|
|
218
|
+
const completedTodos = actualTodos.filter(t => t.status === 'completed');
|
|
219
|
+
const totalCount = actualTodos.length;
|
|
220
|
+
const completedCount = completedTodos.length;
|
|
221
|
+
|
|
222
|
+
// Find latest completed task
|
|
223
|
+
const latestCompleted = completedTodos[completedTodos.length - 1];
|
|
224
|
+
const latestTask = latestCompleted?.content || null;
|
|
225
|
+
|
|
226
|
+
// Check if progress changed (more tasks completed than before)
|
|
227
|
+
const previousCompleted = progress.completedTasks?.length || 0;
|
|
228
|
+
|
|
229
|
+
if (completedCount > previousCompleted) {
|
|
230
|
+
// Update GitHub issue
|
|
231
|
+
updateGitHubIssue(progress.linkedIssue, completedCount, totalCount, latestTask, config);
|
|
232
|
+
|
|
233
|
+
// Update progress tracking
|
|
234
|
+
progress.completedTasks = completedTodos.map(t => t.content);
|
|
235
|
+
progress.tasks = actualTodos.map(t => ({ content: t.content, status: t.status }));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Save progress
|
|
239
|
+
saveProgress(progress);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Run the hook
|
|
243
|
+
main().catch(err => {
|
|
244
|
+
// Silently fail - don't break Claude Code
|
|
245
|
+
if (process.env.DEBUG) {
|
|
246
|
+
console.error('github-progress hook error:', err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Progress Hook
|
|
3
|
-
*
|
|
4
|
-
* Automatically updates GitHub issues as tasks are completed.
|
|
5
|
-
* Monitors TodoWrite/TaskUpdate calls and syncs progress to linked GitHub issues.
|
|
6
|
-
*
|
|
7
|
-
* Event: PostToolUse
|
|
8
|
-
* Priority: {{hooks.priorities.automation}}
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const { execSync } = require('child_process');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
// Configuration from tech-stack.json
|
|
16
|
-
const CONFIG = {
|
|
17
|
-
owner: '{{versionControl.owner}}',
|
|
18
|
-
repo: '{{versionControl.repo}}',
|
|
19
|
-
projectNumber: {{versionControl.projectBoard.number}},
|
|
20
|
-
enabled: '{{versionControl.projectBoard.type}}' === 'github-projects',
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const PROGRESS_FILE = '.claude/hooks/cache/github-progress.json';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load progress tracking data
|
|
27
|
-
*/
|
|
28
|
-
function loadProgress() {
|
|
29
|
-
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
30
|
-
|
|
31
|
-
if (fs.existsSync(progressPath)) {
|
|
32
|
-
try {
|
|
33
|
-
return JSON.parse(fs.readFileSync(progressPath, 'utf8'));
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.warn('[github-progress] Could not parse progress file');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
linkedIssue: null,
|
|
41
|
-
tasks: [],
|
|
42
|
-
completedTasks: [],
|
|
43
|
-
lastUpdate: null,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Save progress tracking data
|
|
49
|
-
*/
|
|
50
|
-
function saveProgress(progress) {
|
|
51
|
-
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
52
|
-
const progressDir = path.dirname(progressPath);
|
|
53
|
-
|
|
54
|
-
if (!fs.existsSync(progressDir)) {
|
|
55
|
-
fs.mkdirSync(progressDir, { recursive: true });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
progress.lastUpdate = new Date().toISOString();
|
|
59
|
-
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Check if gh CLI is available
|
|
64
|
-
*/
|
|
65
|
-
function hasGhCli() {
|
|
66
|
-
try {
|
|
67
|
-
execSync('gh --version', { stdio: 'ignore' });
|
|
68
|
-
return true;
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Update GitHub issue with progress
|
|
76
|
-
*/
|
|
77
|
-
function updateGitHubIssue(issueNumber, completedTasks, totalTasks, latestTask) {
|
|
78
|
-
if (!hasGhCli()) {
|
|
79
|
-
console.warn('[github-progress] gh CLI not available');
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
// Create progress comment
|
|
85
|
-
const percentage = Math.round((completedTasks / totalTasks) * 100);
|
|
86
|
-
const progressBar = '█'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10));
|
|
87
|
-
|
|
88
|
-
const comment = `### Progress Update
|
|
89
|
-
|
|
90
|
-
${progressBar} ${percentage}% (${completedTasks}/${totalTasks} tasks)
|
|
91
|
-
|
|
92
|
-
**Latest completed:** ${latestTask || 'N/A'}
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
*Auto-updated by Claude Code github-progress-hook*`;
|
|
96
|
-
|
|
97
|
-
// Add comment to issue
|
|
98
|
-
execSync(
|
|
99
|
-
`gh issue comment ${issueNumber} --repo ${CONFIG.owner}/${CONFIG.repo} --body "${comment.replace(/"/g, '\\"')}"`,
|
|
100
|
-
{ stdio: 'ignore' }
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return true;
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.warn('[github-progress] Failed to update GitHub:', error.message);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extract linked issue from task metadata or context
|
|
112
|
-
*/
|
|
113
|
-
function findLinkedIssue(input, progress) {
|
|
114
|
-
// Check if issue is already linked
|
|
115
|
-
if (progress.linkedIssue) {
|
|
116
|
-
return progress.linkedIssue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Try to extract from task description
|
|
120
|
-
if (input && input.description) {
|
|
121
|
-
const issueMatch = input.description.match(/#(\d+)/);
|
|
122
|
-
if (issueMatch) {
|
|
123
|
-
return parseInt(issueMatch[1], 10);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Try to extract from subject
|
|
128
|
-
if (input && input.subject) {
|
|
129
|
-
const issueMatch = input.subject.match(/#(\d+)/);
|
|
130
|
-
if (issueMatch) {
|
|
131
|
-
return parseInt(issueMatch[1], 10);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Main hook handler
|
|
140
|
-
*/
|
|
141
|
-
module.exports = async function githubProgressHook(context) {
|
|
142
|
-
// Skip if not configured
|
|
143
|
-
if (!CONFIG.enabled || !CONFIG.owner || !CONFIG.repo) {
|
|
144
|
-
return { continue: true };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const { tool, input } = context;
|
|
148
|
-
|
|
149
|
-
// Only process task-related tools
|
|
150
|
-
if (!['TodoWrite', 'TaskUpdate', 'TaskCreate'].includes(tool)) {
|
|
151
|
-
return { continue: true };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Load progress tracking
|
|
155
|
-
const progress = loadProgress();
|
|
156
|
-
|
|
157
|
-
// Check for linked issue
|
|
158
|
-
const issueNumber = findLinkedIssue(input, progress);
|
|
159
|
-
|
|
160
|
-
if (issueNumber && !progress.linkedIssue) {
|
|
161
|
-
progress.linkedIssue = issueNumber;
|
|
162
|
-
console.log(`[github-progress] Linked to issue #${issueNumber}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Track task completion
|
|
166
|
-
if (tool === 'TaskUpdate' && input && input.status === 'completed') {
|
|
167
|
-
const taskId = input.taskId;
|
|
168
|
-
if (taskId && !progress.completedTasks.includes(taskId)) {
|
|
169
|
-
progress.completedTasks.push(taskId);
|
|
170
|
-
|
|
171
|
-
// Update GitHub if we have a linked issue
|
|
172
|
-
if (progress.linkedIssue) {
|
|
173
|
-
const totalTasks = progress.tasks.length || progress.completedTasks.length;
|
|
174
|
-
updateGitHubIssue(
|
|
175
|
-
progress.linkedIssue,
|
|
176
|
-
progress.completedTasks.length,
|
|
177
|
-
totalTasks,
|
|
178
|
-
input.subject || `Task ${taskId}`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Track new tasks
|
|
185
|
-
if (tool === 'TaskCreate' && input && input.subject) {
|
|
186
|
-
progress.tasks.push({
|
|
187
|
-
id: Date.now().toString(),
|
|
188
|
-
subject: input.subject,
|
|
189
|
-
created: new Date().toISOString(),
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Save updated progress
|
|
194
|
-
saveProgress(progress);
|
|
195
|
-
|
|
196
|
-
return { continue: true };
|
|
197
|
-
};
|