codymaster 4.1.1 → 4.1.3
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/CHANGELOG.md +40 -1
- package/README.md +108 -35
- package/dist/dashboard.js +2 -1
- package/dist/index.js +263 -158
- package/dist/ui/box.js +188 -0
- package/dist/ui/hamster.js +223 -0
- package/dist/ui/hooks.js +253 -0
- package/dist/ui/onboarding.js +315 -0
- package/dist/ui/theme.js +105 -0
- package/install.sh +143 -64
- package/package.json +7 -7
- package/skills/cm-code-review/SKILL.md +27 -0
- package/skills/cm-execution/SKILL.md +38 -0
- package/skills/cm-planning/SKILL.md +4 -0
- package/skills/cm-project-bootstrap/SKILL.md +7 -1
- package/skills/cm-quality-gate/SKILL.md +67 -0
- package/skills/cm-safe-i18n/SKILL.md +4 -1
- package/skills/cm-tdd/SKILL.md +38 -0
package/dist/ui/box.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 📦 Box Drawing — Terminal UI components
|
|
4
|
+
* Bordered panels, tables, progress bars, badges
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.termWidth = termWidth;
|
|
11
|
+
exports.renderBox = renderBox;
|
|
12
|
+
exports.renderDivider = renderDivider;
|
|
13
|
+
exports.renderTable = renderTable;
|
|
14
|
+
exports.renderProgressBar = renderProgressBar;
|
|
15
|
+
exports.renderBadge = renderBadge;
|
|
16
|
+
exports.renderPriority = renderPriority;
|
|
17
|
+
exports.renderSpeechBubble = renderSpeechBubble;
|
|
18
|
+
exports.renderStepProgress = renderStepProgress;
|
|
19
|
+
exports.renderFooter = renderFooter;
|
|
20
|
+
exports.stripAnsi = stripAnsi;
|
|
21
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
22
|
+
const theme_1 = require("./theme");
|
|
23
|
+
// ─── Terminal Width ────────────────────────────────────────────────────────
|
|
24
|
+
function termWidth() {
|
|
25
|
+
return process.stdout.columns || 80;
|
|
26
|
+
}
|
|
27
|
+
function clamp(val, min, max) {
|
|
28
|
+
return Math.max(min, Math.min(max, val));
|
|
29
|
+
}
|
|
30
|
+
// ─── Box Components ────────────────────────────────────────────────────────
|
|
31
|
+
const BOX = {
|
|
32
|
+
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
33
|
+
h: '─', v: '│',
|
|
34
|
+
// Table
|
|
35
|
+
ttl: '┌', ttr: '┐', tbl: '└', tbr: '┘',
|
|
36
|
+
th: '─', tv: '│', tc: '┼',
|
|
37
|
+
tlt: '├', trt: '┤', ttt: '┬', tbt: '┴',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Render a bordered box with optional title
|
|
41
|
+
*/
|
|
42
|
+
function renderBox(content, opts) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const pad = (_a = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _a !== void 0 ? _a : 1;
|
|
45
|
+
const w = (_b = opts === null || opts === void 0 ? void 0 : opts.width) !== null && _b !== void 0 ? _b : clamp(termWidth() - 4, 40, 80);
|
|
46
|
+
const innerW = w - 2 - (pad * 2);
|
|
47
|
+
const lines = [];
|
|
48
|
+
const padStr = ' '.repeat(pad);
|
|
49
|
+
// Top border with optional title
|
|
50
|
+
if (opts === null || opts === void 0 ? void 0 : opts.title) {
|
|
51
|
+
const title = ` ${opts.title} `;
|
|
52
|
+
const leftLen = 2;
|
|
53
|
+
const rightLen = Math.max(0, w - 2 - leftLen - stripAnsi(title).length);
|
|
54
|
+
lines.push((0, theme_1.dim)(`${BOX.tl}${BOX.h.repeat(leftLen)}`) + (0, theme_1.brand)(title) + (0, theme_1.dim)(`${BOX.h.repeat(rightLen)}${BOX.tr}`));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
lines.push((0, theme_1.dim)(`${BOX.tl}${BOX.h.repeat(w - 2)}${BOX.tr}`));
|
|
58
|
+
}
|
|
59
|
+
// Content lines
|
|
60
|
+
for (const line of content) {
|
|
61
|
+
const visible = stripAnsi(line);
|
|
62
|
+
const spaces = Math.max(0, innerW - visible.length);
|
|
63
|
+
lines.push((0, theme_1.dim)(BOX.v) + padStr + line + ' '.repeat(spaces) + padStr + (0, theme_1.dim)(BOX.v));
|
|
64
|
+
}
|
|
65
|
+
// Bottom border
|
|
66
|
+
lines.push((0, theme_1.dim)(`${BOX.bl}${BOX.h.repeat(w - 2)}${BOX.br}`));
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Render a simple divider line
|
|
71
|
+
*/
|
|
72
|
+
function renderDivider(width) {
|
|
73
|
+
const w = width !== null && width !== void 0 ? width : clamp(termWidth() - 4, 40, 80);
|
|
74
|
+
return (0, theme_1.dim)(BOX.h.repeat(w));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Render a clean bordered table
|
|
78
|
+
*/
|
|
79
|
+
function renderTable(columns, rows) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
const totalW = columns.reduce((sum, c) => sum + c.width, 0) + columns.length + 1;
|
|
82
|
+
// Header separator
|
|
83
|
+
const headerSep = (0, theme_1.dim)(columns.map(c => BOX.th.repeat(c.width)).join((0, theme_1.dim)('─┬─')));
|
|
84
|
+
lines.push(` ${headerSep}`);
|
|
85
|
+
// Header row
|
|
86
|
+
const headerCells = columns.map(c => {
|
|
87
|
+
const text = padCell(c.header, c.width, c.align);
|
|
88
|
+
return (0, theme_1.dim)(text);
|
|
89
|
+
});
|
|
90
|
+
lines.push(` ${headerCells.join((0, theme_1.dim)(' │ '))}`);
|
|
91
|
+
// Header underline
|
|
92
|
+
lines.push(` ${headerSep}`);
|
|
93
|
+
// Data rows
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const cells = columns.map(c => {
|
|
96
|
+
const val = row[c.header] || '—';
|
|
97
|
+
const display = padCell(stripAnsi(val).length > c.width ? stripAnsi(val).substring(0, c.width - 1) + '…' : val, c.width, c.align);
|
|
98
|
+
return c.color ? c.color(display) : display;
|
|
99
|
+
});
|
|
100
|
+
lines.push(` ${cells.join((0, theme_1.dim)(' │ '))}`);
|
|
101
|
+
}
|
|
102
|
+
// Bottom separator
|
|
103
|
+
lines.push(` ${headerSep}`);
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
function padCell(str, width, align) {
|
|
107
|
+
const visible = stripAnsi(str);
|
|
108
|
+
const diff = Math.max(0, width - visible.length);
|
|
109
|
+
if (align === 'right')
|
|
110
|
+
return ' '.repeat(diff) + str;
|
|
111
|
+
if (align === 'center') {
|
|
112
|
+
const left = Math.floor(diff / 2);
|
|
113
|
+
return ' '.repeat(left) + str + ' '.repeat(diff - left);
|
|
114
|
+
}
|
|
115
|
+
return str + ' '.repeat(diff);
|
|
116
|
+
}
|
|
117
|
+
// ─── Progress Bar ──────────────────────────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* Render a colored progress bar
|
|
120
|
+
*/
|
|
121
|
+
function renderProgressBar(pct, width) {
|
|
122
|
+
const w = width !== null && width !== void 0 ? width : 16;
|
|
123
|
+
const filled = Math.round((clamp(pct, 0, 100) / 100) * w);
|
|
124
|
+
const empty = w - filled;
|
|
125
|
+
const color = pct >= 100 ? theme_1.success : pct >= 60 ? chalk_1.default.hex(theme_1.COLORS.success) : pct >= 30 ? theme_1.warning : theme_1.error;
|
|
126
|
+
return color('█'.repeat(filled)) + (0, theme_1.muted)('░'.repeat(empty)) + (0, theme_1.dim)(` ${pct}%`);
|
|
127
|
+
}
|
|
128
|
+
// ─── Status Badge ──────────────────────────────────────────────────────────
|
|
129
|
+
const BADGE_COLORS = {
|
|
130
|
+
'backlog': chalk_1.default.hex(theme_1.COLORS.backlog),
|
|
131
|
+
'in-progress': chalk_1.default.hex(theme_1.COLORS.inProgress),
|
|
132
|
+
'review': chalk_1.default.hex(theme_1.COLORS.warning),
|
|
133
|
+
'done': chalk_1.default.hex(theme_1.COLORS.done),
|
|
134
|
+
'success': chalk_1.default.hex(theme_1.COLORS.success),
|
|
135
|
+
'failed': chalk_1.default.hex(theme_1.COLORS.error),
|
|
136
|
+
'pending': chalk_1.default.hex(theme_1.COLORS.warning),
|
|
137
|
+
'running': chalk_1.default.hex(theme_1.COLORS.inProgress),
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Render a colored status badge: ● status
|
|
141
|
+
*/
|
|
142
|
+
function renderBadge(status) {
|
|
143
|
+
const color = BADGE_COLORS[status] || theme_1.dim;
|
|
144
|
+
return color(`${theme_1.ICONS.dot} ${status}`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Render priority badge
|
|
148
|
+
*/
|
|
149
|
+
function renderPriority(priority) {
|
|
150
|
+
const color = theme_1.PRI[priority] || theme_1.dim;
|
|
151
|
+
return color(`${theme_1.ICONS.dot} ${priority}`);
|
|
152
|
+
}
|
|
153
|
+
// ─── Speech Bubble ─────────────────────────────────────────────────────────
|
|
154
|
+
/**
|
|
155
|
+
* Render a hamster speech bubble
|
|
156
|
+
*/
|
|
157
|
+
function renderSpeechBubble(message) {
|
|
158
|
+
const w = stripAnsi(message).length + 4;
|
|
159
|
+
return [
|
|
160
|
+
(0, theme_1.dim)(` ╭${'─'.repeat(w)}╮`),
|
|
161
|
+
(0, theme_1.dim)(` │`) + ` ${message} ` + (0, theme_1.dim)(`│`),
|
|
162
|
+
(0, theme_1.dim)(` ╰${'─'.repeat(w)}╯`),
|
|
163
|
+
(0, theme_1.dim)(` ╰─`),
|
|
164
|
+
].join('\n');
|
|
165
|
+
}
|
|
166
|
+
// ─── Step Indicator ────────────────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Render onboarding step progress: Step 2 of 5 ●●○○○
|
|
169
|
+
*/
|
|
170
|
+
function renderStepProgress(current, total) {
|
|
171
|
+
const dots = Array.from({ length: total }, (_, i) => i < current ? (0, theme_1.brand)(theme_1.ICONS.dot) : (0, theme_1.muted)(theme_1.ICONS.dotEmpty)).join(' ');
|
|
172
|
+
return ` ${(0, theme_1.dim)('Step')} ${(0, theme_1.brand)(String(current))} ${(0, theme_1.dim)('of')} ${(0, theme_1.dim)(String(total))} ${dots}`;
|
|
173
|
+
}
|
|
174
|
+
// ─── Footer ────────────────────────────────────────────────────────────────
|
|
175
|
+
/**
|
|
176
|
+
* Render a footer hint bar
|
|
177
|
+
*/
|
|
178
|
+
function renderFooter(hints) {
|
|
179
|
+
return ` ${hints.map(h => (0, theme_1.dim)(h)).join((0, theme_1.dim)(' • '))}`;
|
|
180
|
+
}
|
|
181
|
+
// ─── Utilities ─────────────────────────────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Strip ANSI escape codes for width calculations
|
|
184
|
+
*/
|
|
185
|
+
function stripAnsi(str) {
|
|
186
|
+
// eslint-disable-next-line no-control-regex
|
|
187
|
+
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1B\]8;[^;]*;[^\x1B]*\x1B\\/g, '');
|
|
188
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🐹 Hamster Mascot — ASCII art + personality system
|
|
4
|
+
* The face of CodyMaster CLI
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getHamsterArt = getHamsterArt;
|
|
8
|
+
exports.getGreeting = getGreeting;
|
|
9
|
+
exports.getCelebration = getCelebration;
|
|
10
|
+
exports.getEncouragement = getEncouragement;
|
|
11
|
+
exports.getErrorGuidance = getErrorGuidance;
|
|
12
|
+
exports.renderHamsterBanner = renderHamsterBanner;
|
|
13
|
+
exports.renderHamsterMessage = renderHamsterMessage;
|
|
14
|
+
const theme_1 = require("./theme");
|
|
15
|
+
// ─── ASCII Art States ──────────────────────────────────────────────────────
|
|
16
|
+
const HAMSTER_ART = {
|
|
17
|
+
happy: [
|
|
18
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')}`,
|
|
19
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.brandBold)('^ ^')} ${(0, theme_1.brand)('\\')}`,
|
|
20
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.brandBold)('u')} ${(0, theme_1.brand)(')')}`,
|
|
21
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
22
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
23
|
+
],
|
|
24
|
+
angry: [
|
|
25
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')}`,
|
|
26
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.warning)('> <')} ${(0, theme_1.brand)('\\')}`,
|
|
27
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.warning)('x')} ${(0, theme_1.brand)(')')}`,
|
|
28
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
29
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
30
|
+
],
|
|
31
|
+
sad: [
|
|
32
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')}`,
|
|
33
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.dim)('u u')} ${(0, theme_1.brand)('\\')}`,
|
|
34
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.dim)('n')} ${(0, theme_1.brand)(')')}`,
|
|
35
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
36
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
37
|
+
],
|
|
38
|
+
surprised: [
|
|
39
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')}`,
|
|
40
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.info)('O O')} ${(0, theme_1.brand)('\\')}`,
|
|
41
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.info)('o')} ${(0, theme_1.brand)(')')}`,
|
|
42
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
43
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
44
|
+
],
|
|
45
|
+
in_love: [
|
|
46
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')} ${(0, theme_1.success)('♥')}`,
|
|
47
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.success)('* *')} ${(0, theme_1.brand)('\\')}`,
|
|
48
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.success)('v')} ${(0, theme_1.brand)(')')}`,
|
|
49
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
50
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
51
|
+
],
|
|
52
|
+
sleeping: [
|
|
53
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')} ${(0, theme_1.dim)('zZ')}`,
|
|
54
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.dim)('- -')} ${(0, theme_1.brand)('\\')} ${(0, theme_1.dim)('zZ')}`,
|
|
55
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.dim)('u')} ${(0, theme_1.brand)(')')}`,
|
|
56
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
57
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
58
|
+
],
|
|
59
|
+
thinking: [
|
|
60
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')} ${(0, theme_1.dim)('.oO')}`,
|
|
61
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.dim)('. .')} ${(0, theme_1.brand)('\\')} ${(0, theme_1.dim)('/')}`,
|
|
62
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.dim)('u')} ${(0, theme_1.brand)(')')}`,
|
|
63
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
64
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
65
|
+
],
|
|
66
|
+
cool: [
|
|
67
|
+
` ${(0, theme_1.brand)('( . \\ --- / . )')}`,
|
|
68
|
+
` ${(0, theme_1.brand)('/')} ${(0, theme_1.info)('B B')} ${(0, theme_1.brand)('\\')}`,
|
|
69
|
+
` ${(0, theme_1.brand)('(')} ${(0, theme_1.info)('v')} ${(0, theme_1.brand)(')')}`,
|
|
70
|
+
` ${(0, theme_1.brand)('| \\ ___ / |')}`,
|
|
71
|
+
` ${(0, theme_1.brand)('\'--w---w--\'')}`,
|
|
72
|
+
],
|
|
73
|
+
celebrating: [
|
|
74
|
+
` ${(0, theme_1.success)('\\')} ${(0, theme_1.brand)('( \\_/ )')} ${(0, theme_1.success)('/')}`,
|
|
75
|
+
` ${(0, theme_1.success)('\\')} ${(0, theme_1.brand)('(')} ${(0, theme_1.success)('^ u ^')} ${(0, theme_1.brand)(')')} ${(0, theme_1.success)('/')}`,
|
|
76
|
+
` ${(0, theme_1.success)('--')} ${(0, theme_1.brand)('( ___ )')} ${(0, theme_1.success)('--')}`,
|
|
77
|
+
` ${(0, theme_1.brand)('| [ ] |')}`,
|
|
78
|
+
` ${(0, theme_1.brand)('\'--w-w--\'')}`,
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
// Aliases for compatibility with existing code
|
|
82
|
+
Object.assign(HAMSTER_ART, {
|
|
83
|
+
greeting: HAMSTER_ART.happy,
|
|
84
|
+
working: HAMSTER_ART.thinking,
|
|
85
|
+
error: HAMSTER_ART.angry,
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Get hamster ASCII art for a given state
|
|
89
|
+
*/
|
|
90
|
+
function getHamsterArt(state = 'greeting') {
|
|
91
|
+
return HAMSTER_ART[state].join('\n');
|
|
92
|
+
}
|
|
93
|
+
// ─── Time-Based Greetings ──────────────────────────────────────────────────
|
|
94
|
+
function getTimeOfDay() {
|
|
95
|
+
const h = new Date().getHours();
|
|
96
|
+
if (h >= 5 && h < 12)
|
|
97
|
+
return 'morning';
|
|
98
|
+
if (h >= 12 && h < 17)
|
|
99
|
+
return 'afternoon';
|
|
100
|
+
if (h >= 17 && h < 21)
|
|
101
|
+
return 'evening';
|
|
102
|
+
return 'night';
|
|
103
|
+
}
|
|
104
|
+
const TIME_GREETINGS = {
|
|
105
|
+
morning: [
|
|
106
|
+
'Good morning! ☀️ Ready to build?',
|
|
107
|
+
'Rise and code! ☀️',
|
|
108
|
+
'Morning! Let\'s ship something today ☀️',
|
|
109
|
+
],
|
|
110
|
+
afternoon: [
|
|
111
|
+
'Good afternoon! 🌤️ What are we building?',
|
|
112
|
+
'Afternoon! Time to make progress 🏗️',
|
|
113
|
+
'Hey there! Productive day so far? 🌤️',
|
|
114
|
+
],
|
|
115
|
+
evening: [
|
|
116
|
+
'Good evening! 🌅 Wrapping up?',
|
|
117
|
+
'Evening session! 🌙 Let\'s finish strong',
|
|
118
|
+
'Hey! Late push tonight? 🌅',
|
|
119
|
+
],
|
|
120
|
+
night: [
|
|
121
|
+
'Working late? 🌙 Don\'t forget to rest!',
|
|
122
|
+
'Night owl mode! 🦉 I\'m here for you',
|
|
123
|
+
'Midnight coding session? 🌙 Let\'s go!',
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Get a greeting based on time of day + optional user name
|
|
128
|
+
*/
|
|
129
|
+
function getGreeting(userName) {
|
|
130
|
+
const tod = getTimeOfDay();
|
|
131
|
+
const greetings = TIME_GREETINGS[tod];
|
|
132
|
+
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
|
133
|
+
if (userName) {
|
|
134
|
+
return `${greeting.split('!')[0]}, ${userName}! ${greeting.includes('!') ? greeting.split('!').slice(1).join('!').trim() : ''}`.trim();
|
|
135
|
+
}
|
|
136
|
+
return greeting;
|
|
137
|
+
}
|
|
138
|
+
// ─── Hamster Messages ──────────────────────────────────────────────────────
|
|
139
|
+
const CELEBRATIONS = [
|
|
140
|
+
'Nice work! 🎉',
|
|
141
|
+
'You\'re on fire! 🔥',
|
|
142
|
+
'Ship it! 🚀',
|
|
143
|
+
'Another one bites the dust! ✅',
|
|
144
|
+
'Level up! ⬆️',
|
|
145
|
+
'Crushing it! 💪',
|
|
146
|
+
'That\'s how it\'s done! ⭐',
|
|
147
|
+
'Clean execution! 🎯',
|
|
148
|
+
'Boom! Done! 💥',
|
|
149
|
+
'Progress! Keep going! 📈',
|
|
150
|
+
'Smooth operator! 🎵',
|
|
151
|
+
'Task terminated! 🤖',
|
|
152
|
+
'One step closer! 🏁',
|
|
153
|
+
'Brilliant move! ♟️',
|
|
154
|
+
'Unstoppable! ⚡',
|
|
155
|
+
];
|
|
156
|
+
const ENCOURAGEMENTS = [
|
|
157
|
+
'You got this! 💪',
|
|
158
|
+
'Almost there! 🏁',
|
|
159
|
+
'Keep pushing! Every step counts 🐾',
|
|
160
|
+
'Rome wasn\'t built in a day, but they were laying bricks! 🧱',
|
|
161
|
+
'Progress, not perfection! 📈',
|
|
162
|
+
'One command at a time 🐹',
|
|
163
|
+
];
|
|
164
|
+
const ERROR_GUIDANCE = [
|
|
165
|
+
'Oops! Let me help you fix that 🔧',
|
|
166
|
+
'Not quite! Here\'s what to try instead 💡',
|
|
167
|
+
'Hmm, that didn\'t work. But we\'ll figure it out! 🐹',
|
|
168
|
+
'Small detour! Let\'s get back on track 🛤️',
|
|
169
|
+
];
|
|
170
|
+
const IDLE_MESSAGES = [
|
|
171
|
+
'Any tasks to tackle? Type cm task list 📋',
|
|
172
|
+
'Need a skill? Try cm list 🧩',
|
|
173
|
+
'Been a while! What are we building? 🏗️',
|
|
174
|
+
];
|
|
175
|
+
/**
|
|
176
|
+
* Get a random celebration message
|
|
177
|
+
*/
|
|
178
|
+
function getCelebration() {
|
|
179
|
+
return CELEBRATIONS[Math.floor(Math.random() * CELEBRATIONS.length)];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get a random encouragement
|
|
183
|
+
*/
|
|
184
|
+
function getEncouragement() {
|
|
185
|
+
return ENCOURAGEMENTS[Math.floor(Math.random() * ENCOURAGEMENTS.length)];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get error guidance
|
|
189
|
+
*/
|
|
190
|
+
function getErrorGuidance() {
|
|
191
|
+
return ERROR_GUIDANCE[Math.floor(Math.random() * ERROR_GUIDANCE.length)];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Render the full hamster banner with greeting
|
|
195
|
+
*/
|
|
196
|
+
function renderHamsterBanner(userName, version, cwd) {
|
|
197
|
+
const art = getHamsterArt(getTimeOfDay() === 'night' ? 'sleeping' : 'greeting');
|
|
198
|
+
const greeting = getGreeting(userName);
|
|
199
|
+
const lines = [
|
|
200
|
+
'',
|
|
201
|
+
art,
|
|
202
|
+
'',
|
|
203
|
+
` ${(0, theme_1.brandBold)(greeting)}`,
|
|
204
|
+
'',
|
|
205
|
+
` ${(0, theme_1.dim)('CodyMaster')} ${(0, theme_1.brand)(`v${version || '?'}`)} ${(0, theme_1.dim)('•')} ${(0, theme_1.dim)('34 Skills')} ${(0, theme_1.dim)('•')} ${(0, theme_1.dim)(cwd || '~')}`,
|
|
206
|
+
(0, theme_1.dim)(' ' + '─'.repeat(50)),
|
|
207
|
+
];
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Render a compact hamster message (for inline use)
|
|
212
|
+
*/
|
|
213
|
+
function renderHamsterMessage(message, state = 'greeting') {
|
|
214
|
+
const art = HAMSTER_ART[state];
|
|
215
|
+
const artWidth = 14;
|
|
216
|
+
// Combine art with message on the same line
|
|
217
|
+
return [
|
|
218
|
+
art[0],
|
|
219
|
+
art[1],
|
|
220
|
+
`${art[2]} ${(0, theme_1.text)(message)}`,
|
|
221
|
+
art[3],
|
|
222
|
+
].join('\n');
|
|
223
|
+
}
|
package/dist/ui/hooks.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🪝 Hook Engine — Trigger → Action → Variable Reward → Investment
|
|
4
|
+
* Based on Nir Eyal's Hook Model for habit-forming CLI experience
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.loadProfile = loadProfile;
|
|
11
|
+
exports.saveProfile = saveProfile;
|
|
12
|
+
exports.isFirstRun = isFirstRun;
|
|
13
|
+
exports.getContextualTrigger = getContextualTrigger;
|
|
14
|
+
exports.checkAchievements = checkAchievements;
|
|
15
|
+
exports.formatAchievement = formatAchievement;
|
|
16
|
+
exports.recordCommand = recordCommand;
|
|
17
|
+
exports.getLevelDisplay = getLevelDisplay;
|
|
18
|
+
exports.formatProfileSummary = formatProfileSummary;
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const os_1 = __importDefault(require("os"));
|
|
22
|
+
const theme_1 = require("./theme");
|
|
23
|
+
const theme_2 = require("./theme");
|
|
24
|
+
// ─── Profile Storage ───────────────────────────────────────────────────────
|
|
25
|
+
const PROFILE_DIR = path_1.default.join(os_1.default.homedir(), '.codymaster');
|
|
26
|
+
const PROFILE_FILE = path_1.default.join(PROFILE_DIR, 'profile.json');
|
|
27
|
+
const DEFAULT_PROFILE = {
|
|
28
|
+
userName: '',
|
|
29
|
+
platform: '',
|
|
30
|
+
onboardingStep: 0,
|
|
31
|
+
onboardingComplete: false,
|
|
32
|
+
firstRunAt: '',
|
|
33
|
+
lastRunAt: '',
|
|
34
|
+
totalCommands: 0,
|
|
35
|
+
streak: 0,
|
|
36
|
+
lastStreakDate: '',
|
|
37
|
+
level: 'beginner',
|
|
38
|
+
achievements: [],
|
|
39
|
+
commandHistory: {},
|
|
40
|
+
skillsUsed: [],
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Load user profile from disk
|
|
44
|
+
*/
|
|
45
|
+
function loadProfile() {
|
|
46
|
+
try {
|
|
47
|
+
if (fs_1.default.existsSync(PROFILE_FILE)) {
|
|
48
|
+
const raw = fs_1.default.readFileSync(PROFILE_FILE, 'utf-8');
|
|
49
|
+
return Object.assign(Object.assign({}, DEFAULT_PROFILE), JSON.parse(raw));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
53
|
+
return Object.assign({}, DEFAULT_PROFILE);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Save user profile to disk
|
|
57
|
+
*/
|
|
58
|
+
function saveProfile(profile) {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs_1.default.existsSync(PROFILE_DIR)) {
|
|
61
|
+
fs_1.default.mkdirSync(PROFILE_DIR, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
fs_1.default.writeFileSync(PROFILE_FILE, JSON.stringify(profile, null, 2));
|
|
64
|
+
}
|
|
65
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if this is the first run (profile doesn't exist)
|
|
69
|
+
*/
|
|
70
|
+
function isFirstRun() {
|
|
71
|
+
return !fs_1.default.existsSync(PROFILE_FILE);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get a contextual trigger message based on user data
|
|
75
|
+
* Hook Model: External trigger that becomes internal over time
|
|
76
|
+
*/
|
|
77
|
+
function getContextualTrigger(profile, ctx) {
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const lastRun = profile.lastRunAt ? new Date(profile.lastRunAt) : null;
|
|
80
|
+
const hoursSinceLastRun = lastRun ? (now.getTime() - lastRun.getTime()) / 3600000 : 999;
|
|
81
|
+
// Re-engagement trigger
|
|
82
|
+
if (hoursSinceLastRun > 48) {
|
|
83
|
+
return `Haven't seen you in a while! 👋 Let's get back to it`;
|
|
84
|
+
}
|
|
85
|
+
// Streak trigger
|
|
86
|
+
if (profile.streak >= 3) {
|
|
87
|
+
return `${theme_2.ICONS.fire} ${profile.streak}-day streak! Keep it going!`;
|
|
88
|
+
}
|
|
89
|
+
// Task-based triggers
|
|
90
|
+
if ((ctx === null || ctx === void 0 ? void 0 : ctx.tasksInProgress) && ctx.tasksInProgress > 0) {
|
|
91
|
+
return `${ctx.tasksInProgress} task${ctx.tasksInProgress > 1 ? 's' : ''} cooking! ${theme_2.ICONS.hamster}`;
|
|
92
|
+
}
|
|
93
|
+
if ((ctx === null || ctx === void 0 ? void 0 : ctx.tasksInReview) && ctx.tasksInReview > 0) {
|
|
94
|
+
return `${ctx.tasksInReview} task${ctx.tasksInReview > 1 ? 's' : ''} waiting for review 👀`;
|
|
95
|
+
}
|
|
96
|
+
if ((ctx === null || ctx === void 0 ? void 0 : ctx.totalTasks) === 0) {
|
|
97
|
+
return `Clean slate! What shall we build? 🏗️`;
|
|
98
|
+
}
|
|
99
|
+
if ((ctx === null || ctx === void 0 ? void 0 : ctx.tasksDone) && ctx.totalTasks && ctx.tasksDone === ctx.totalTasks) {
|
|
100
|
+
return `All clear! ${theme_2.ICONS.star} What's next?`;
|
|
101
|
+
}
|
|
102
|
+
// Default
|
|
103
|
+
return `Ready when you are! ${theme_2.ICONS.hamster}`;
|
|
104
|
+
}
|
|
105
|
+
// ─── VARIABLE REWARD: Achievement System ───────────────────────────────────
|
|
106
|
+
const ACHIEVEMENTS = {
|
|
107
|
+
'first_run': { name: 'Hello World', emoji: '👋', desc: 'First time running CodyMaster' },
|
|
108
|
+
'first_task': { name: 'Task Master', emoji: '✅', desc: 'Created your first task' },
|
|
109
|
+
'first_done': { name: 'Shipper', emoji: '🚀', desc: 'Completed your first task' },
|
|
110
|
+
'five_tasks': { name: 'Busy Bee', emoji: '🐝', desc: 'Created 5 tasks' },
|
|
111
|
+
'first_deploy': { name: 'Deploy Hero', emoji: '🦸', desc: 'First deployment recorded' },
|
|
112
|
+
'first_skill': { name: 'Skill Hunter', emoji: '🧩', desc: 'Used your first skill' },
|
|
113
|
+
'streak_3': { name: 'On Fire', emoji: '🔥', desc: '3-day usage streak' },
|
|
114
|
+
'streak_7': { name: 'Committed', emoji: '💎', desc: '7-day usage streak' },
|
|
115
|
+
'commands_50': { name: 'Power User', emoji: '⚡', desc: '50 commands executed' },
|
|
116
|
+
'commands_100': { name: 'CLI Ninja', emoji: '🥷', desc: '100 commands executed' },
|
|
117
|
+
'level_builder': { name: 'Builder', emoji: '🏗️', desc: 'Reached Builder level' },
|
|
118
|
+
'level_master': { name: 'Master', emoji: '🏆', desc: 'Reached Master level' },
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Check and unlock new achievements
|
|
122
|
+
* Returns newly unlocked achievements
|
|
123
|
+
*/
|
|
124
|
+
function checkAchievements(profile) {
|
|
125
|
+
const newAchievements = [];
|
|
126
|
+
const check = (id, condition) => {
|
|
127
|
+
if (condition && !profile.achievements.includes(id)) {
|
|
128
|
+
profile.achievements.push(id);
|
|
129
|
+
newAchievements.push(id);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
check('first_run', profile.totalCommands >= 1);
|
|
133
|
+
check('first_task', (profile.commandHistory['task add'] || 0) >= 1);
|
|
134
|
+
check('five_tasks', (profile.commandHistory['task add'] || 0) >= 5);
|
|
135
|
+
check('first_done', (profile.commandHistory['task done'] || 0) >= 1);
|
|
136
|
+
check('first_deploy', (profile.commandHistory['deploy'] || 0) >= 1);
|
|
137
|
+
check('first_skill', profile.skillsUsed.length >= 1);
|
|
138
|
+
check('streak_3', profile.streak >= 3);
|
|
139
|
+
check('streak_7', profile.streak >= 7);
|
|
140
|
+
check('commands_50', profile.totalCommands >= 50);
|
|
141
|
+
check('commands_100', profile.totalCommands >= 100);
|
|
142
|
+
check('level_builder', profile.level === 'builder' || profile.level === 'master' || profile.level === 'legend');
|
|
143
|
+
check('level_master', profile.level === 'master' || profile.level === 'legend');
|
|
144
|
+
return newAchievements;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Format achievement unlock message
|
|
148
|
+
*/
|
|
149
|
+
function formatAchievement(id) {
|
|
150
|
+
const a = ACHIEVEMENTS[id];
|
|
151
|
+
if (!a)
|
|
152
|
+
return '';
|
|
153
|
+
return ` ${a.emoji} ${(0, theme_1.success)('Achievement Unlocked:')} ${(0, theme_1.brand)(a.name)} — ${(0, theme_1.dim)(a.desc)}`;
|
|
154
|
+
}
|
|
155
|
+
// ─── INVESTMENT: Track & Level Up ──────────────────────────────────────────
|
|
156
|
+
/**
|
|
157
|
+
* Record a command execution (Investment phase)
|
|
158
|
+
* Updates profile with usage data → CLI becomes more personal
|
|
159
|
+
*/
|
|
160
|
+
function recordCommand(profile, command) {
|
|
161
|
+
const now = new Date();
|
|
162
|
+
const today = now.toISOString().split('T')[0];
|
|
163
|
+
// Update totals
|
|
164
|
+
profile.totalCommands++;
|
|
165
|
+
profile.commandHistory[command] = (profile.commandHistory[command] || 0) + 1;
|
|
166
|
+
profile.lastRunAt = now.toISOString();
|
|
167
|
+
// First run
|
|
168
|
+
if (!profile.firstRunAt) {
|
|
169
|
+
profile.firstRunAt = now.toISOString();
|
|
170
|
+
}
|
|
171
|
+
// Streak calculation
|
|
172
|
+
if (profile.lastStreakDate !== today) {
|
|
173
|
+
const yesterday = new Date(now);
|
|
174
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
175
|
+
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
176
|
+
if (profile.lastStreakDate === yesterdayStr) {
|
|
177
|
+
profile.streak++;
|
|
178
|
+
}
|
|
179
|
+
else if (profile.lastStreakDate !== today) {
|
|
180
|
+
profile.streak = 1;
|
|
181
|
+
}
|
|
182
|
+
profile.lastStreakDate = today;
|
|
183
|
+
}
|
|
184
|
+
// Level calculation
|
|
185
|
+
updateLevel(profile);
|
|
186
|
+
}
|
|
187
|
+
function updateLevel(profile) {
|
|
188
|
+
const cmds = profile.totalCommands;
|
|
189
|
+
const skills = profile.skillsUsed.length;
|
|
190
|
+
if (cmds >= 100 && skills >= 10) {
|
|
191
|
+
profile.level = 'legend';
|
|
192
|
+
}
|
|
193
|
+
else if (cmds >= 50 && skills >= 5) {
|
|
194
|
+
profile.level = 'master';
|
|
195
|
+
}
|
|
196
|
+
else if (cmds >= 10 && skills >= 1) {
|
|
197
|
+
profile.level = 'builder';
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
profile.level = 'beginner';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get level display with emoji
|
|
205
|
+
*/
|
|
206
|
+
function getLevelDisplay(level) {
|
|
207
|
+
const levels = {
|
|
208
|
+
beginner: { emoji: '🌱', color: theme_1.dim },
|
|
209
|
+
builder: { emoji: '🏗️', color: theme_1.info },
|
|
210
|
+
master: { emoji: '🏆', color: theme_1.brand },
|
|
211
|
+
legend: { emoji: '👑', color: theme_1.success },
|
|
212
|
+
};
|
|
213
|
+
const l = levels[level] || levels.beginner;
|
|
214
|
+
return `${l.emoji} ${l.color(level.charAt(0).toUpperCase() + level.slice(1))}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Format profile summary for `cm profile` command
|
|
218
|
+
*/
|
|
219
|
+
function formatProfileSummary(profile) {
|
|
220
|
+
const lines = [
|
|
221
|
+
'',
|
|
222
|
+
` ${(0, theme_1.brand)('🐹 Your Profile')}`,
|
|
223
|
+
'',
|
|
224
|
+
` ${(0, theme_1.dim)('Name:')} ${profile.userName || (0, theme_1.dim)('(not set)')}`,
|
|
225
|
+
` ${(0, theme_1.dim)('Level:')} ${getLevelDisplay(profile.level)}`,
|
|
226
|
+
` ${(0, theme_1.dim)('Streak:')} ${profile.streak > 0 ? `${theme_2.ICONS.fire} ${(0, theme_1.brand)(String(profile.streak))} days` : (0, theme_1.dim)('Start today!')}`,
|
|
227
|
+
` ${(0, theme_1.dim)('Commands:')} ${(0, theme_1.brand)(String(profile.totalCommands))} total`,
|
|
228
|
+
` ${(0, theme_1.dim)('Platform:')} ${profile.platform || (0, theme_1.dim)('(auto-detect)')}`,
|
|
229
|
+
'',
|
|
230
|
+
];
|
|
231
|
+
// Achievements
|
|
232
|
+
if (profile.achievements.length > 0) {
|
|
233
|
+
lines.push(` ${(0, theme_1.brand)('Achievements')} (${profile.achievements.length}/${Object.keys(ACHIEVEMENTS).length})`);
|
|
234
|
+
for (const id of profile.achievements) {
|
|
235
|
+
const a = ACHIEVEMENTS[id];
|
|
236
|
+
if (a)
|
|
237
|
+
lines.push(` ${a.emoji} ${a.name}`);
|
|
238
|
+
}
|
|
239
|
+
lines.push('');
|
|
240
|
+
}
|
|
241
|
+
// Top commands
|
|
242
|
+
const topCmds = Object.entries(profile.commandHistory)
|
|
243
|
+
.sort(([, a], [, b]) => b - a)
|
|
244
|
+
.slice(0, 5);
|
|
245
|
+
if (topCmds.length > 0) {
|
|
246
|
+
lines.push(` ${(0, theme_1.brand)('Top Commands')}`);
|
|
247
|
+
for (const [cmd, count] of topCmds) {
|
|
248
|
+
lines.push(` ${(0, theme_1.dim)('cm')} ${cmd} ${(0, theme_1.muted)(`×${count}`)}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push('');
|
|
251
|
+
}
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|