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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cli-advanced-starter-pack",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "Advanced Claude Code CLI toolkit - agents, hooks, skills, MCP servers, phased development, and GitHub integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
+ }
@@ -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
+ }
@@ -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
- };