@mbc-cqrs-serverless/cli 1.0.22 → 1.0.24

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/README.md CHANGED
@@ -156,6 +156,64 @@ Add MBC CQRS UI common components to your project.
156
156
  mbc ui-common -p ./src/common-ui -c all
157
157
  ```
158
158
 
159
+ ### `mbc install-skills`
160
+
161
+ Install Claude Code skills for MBC CQRS Serverless development.
162
+
163
+ **Alias**: `skills`
164
+
165
+ **Options**:
166
+ - `-p, --project` - Install to project directory (`.claude/skills/`) instead of personal (`~/.claude/skills/`)
167
+ - `-f, --force` - Overwrite existing skills
168
+ - `-l, --list` - List available skills without installing
169
+ - `-c, --check` - Check if updates are available without installing
170
+
171
+ **Examples**:
172
+
173
+ ```bash
174
+ # Install to personal skills directory (available in all projects)
175
+ mbc install-skills
176
+
177
+ # Install to project directory (shared with team via git)
178
+ mbc install-skills --project
179
+
180
+ # List available skills
181
+ mbc install-skills --list
182
+
183
+ # Force overwrite existing skills
184
+ mbc install-skills --force
185
+
186
+ # Check for updates
187
+ mbc install-skills --check
188
+
189
+ # Using alias
190
+ mbc skills -p
191
+ ```
192
+
193
+ **Upgrading Skills**:
194
+
195
+ Skills do not auto-update. To upgrade to the latest version:
196
+
197
+ ```bash
198
+ # Update CLI to latest version
199
+ npm update -g @mbc-cqrs-serverless/cli
200
+
201
+ # Check if updates are available
202
+ mbc install-skills --check
203
+
204
+ # Force reinstall to get latest version
205
+ mbc install-skills --force
206
+ ```
207
+
208
+ **Available Skills**:
209
+
210
+ | Skill | Description |
211
+ |-------|-------------|
212
+ | `/mbc-generate` | Generate boilerplate code (modules, services, controllers, DTOs, handlers) |
213
+ | `/mbc-review` | Review code for best practices and anti-patterns (20 patterns) |
214
+ | `/mbc-migrate` | Guide version migrations and breaking changes |
215
+ | `/mbc-debug` | Debug and troubleshoot common issues |
216
+
159
217
  ## Project Structure
160
218
 
161
219
  The CLI creates a standardized project structure optimized for CQRS:
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VERSION_FILE_NAME = void 0;
7
+ exports.getSkillsSourcePath = getSkillsSourcePath;
8
+ exports.getPersonalSkillsPath = getPersonalSkillsPath;
9
+ exports.getProjectSkillsPath = getProjectSkillsPath;
10
+ exports.copySkills = copySkills;
11
+ exports.getInstalledVersion = getInstalledVersion;
12
+ exports.getPackageVersion = getPackageVersion;
13
+ exports.writeVersionFile = writeVersionFile;
14
+ exports.default = installSkillsAction;
15
+ const fs_1 = require("fs");
16
+ const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const ui_1 = require("../ui");
19
+ /**
20
+ * Version file name for tracking installed skills version
21
+ */
22
+ exports.VERSION_FILE_NAME = '.mbc-skills-version';
23
+ /**
24
+ * Get the path to the mcp-server skills source directory
25
+ */
26
+ function getSkillsSourcePath() {
27
+ // Try to find the mcp-server package
28
+ const possiblePaths = [
29
+ // When installed globally or locally via npm
30
+ path_1.default.join(__dirname, '..', '..', '..', 'mcp-server', 'skills'),
31
+ // When running from monorepo
32
+ path_1.default.join(__dirname, '..', '..', '..', '..', 'mcp-server', 'skills'),
33
+ // When installed via npm in node_modules
34
+ path_1.default.join(process.cwd(), 'node_modules', '@mbc-cqrs-serverless', 'mcp-server', 'skills'),
35
+ ];
36
+ for (const p of possiblePaths) {
37
+ if ((0, fs_1.existsSync)(p)) {
38
+ return p;
39
+ }
40
+ }
41
+ // Try to resolve from npm package
42
+ try {
43
+ const mcpServerPath = require.resolve('@mbc-cqrs-serverless/mcp-server');
44
+ return path_1.default.join(path_1.default.dirname(mcpServerPath), '..', 'skills');
45
+ }
46
+ catch {
47
+ // Package not found
48
+ }
49
+ throw new Error('Could not find mcp-server skills directory. Please ensure @mbc-cqrs-serverless/mcp-server is installed.');
50
+ }
51
+ /**
52
+ * Get the path to personal skills directory (~/.claude/skills/)
53
+ */
54
+ function getPersonalSkillsPath() {
55
+ return path_1.default.join(os_1.default.homedir(), '.claude', 'skills');
56
+ }
57
+ /**
58
+ * Get the path to project skills directory (.claude/skills/)
59
+ */
60
+ function getProjectSkillsPath() {
61
+ return path_1.default.join(process.cwd(), '.claude', 'skills');
62
+ }
63
+ /**
64
+ * Copy skills from source to destination
65
+ * @returns List of copied skill names
66
+ */
67
+ function copySkills(sourcePath, destPath) {
68
+ // Create destination directory if it doesn't exist
69
+ if (!(0, fs_1.existsSync)(destPath)) {
70
+ (0, fs_1.mkdirSync)(destPath, { recursive: true });
71
+ }
72
+ // Get all skill directories
73
+ const entries = (0, fs_1.readdirSync)(sourcePath, { withFileTypes: true });
74
+ const skillDirs = entries.filter((entry) => entry.isDirectory());
75
+ const copiedSkills = [];
76
+ for (const skillDir of skillDirs) {
77
+ const skillSourcePath = path_1.default.join(sourcePath, skillDir.name);
78
+ const skillDestPath = path_1.default.join(destPath, skillDir.name);
79
+ (0, fs_1.cpSync)(skillSourcePath, skillDestPath, { recursive: true });
80
+ copiedSkills.push(skillDir.name);
81
+ }
82
+ return copiedSkills;
83
+ }
84
+ /**
85
+ * List available skills
86
+ */
87
+ function listSkills(sourcePath) {
88
+ const entries = (0, fs_1.readdirSync)(sourcePath, { withFileTypes: true });
89
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
90
+ }
91
+ /**
92
+ * Get the installed version of skills from the version file
93
+ * @returns The installed version or null if not found
94
+ */
95
+ function getInstalledVersion(destPath) {
96
+ const versionFilePath = path_1.default.join(destPath, exports.VERSION_FILE_NAME);
97
+ if (!(0, fs_1.existsSync)(versionFilePath)) {
98
+ return null;
99
+ }
100
+ try {
101
+ const version = (0, fs_1.readFileSync)(versionFilePath, 'utf-8');
102
+ return version.trim();
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Get the package version from mcp-server package.json
110
+ * @returns The package version or null if not found
111
+ */
112
+ function getPackageVersion(sourcePath) {
113
+ // The package.json is one level up from the skills directory
114
+ const packageJsonPath = path_1.default.join(sourcePath, '..', 'package.json');
115
+ if (!(0, fs_1.existsSync)(packageJsonPath)) {
116
+ return null;
117
+ }
118
+ try {
119
+ const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf-8'));
120
+ return packageJson.version || null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Write the version file to track installed skills version
128
+ */
129
+ function writeVersionFile(destPath, version) {
130
+ const versionFilePath = path_1.default.join(destPath, exports.VERSION_FILE_NAME);
131
+ (0, fs_1.writeFileSync)(versionFilePath, version, 'utf-8');
132
+ }
133
+ /**
134
+ * Install Claude Code skills for MBC CQRS Serverless
135
+ */
136
+ async function installSkillsAction(options, command) {
137
+ if (!command) {
138
+ throw new Error('Command is required');
139
+ }
140
+ ui_1.logger.info(`Executing command '${command.name()}'...`);
141
+ const { project, force, list, check } = options;
142
+ // Get source path
143
+ let sourcePath;
144
+ try {
145
+ sourcePath = getSkillsSourcePath();
146
+ }
147
+ catch (error) {
148
+ ui_1.logger.error(error.message);
149
+ throw error;
150
+ }
151
+ // Verify source exists
152
+ if (!(0, fs_1.existsSync)(sourcePath)) {
153
+ const errorMsg = `Skills source directory not found: ${sourcePath}`;
154
+ ui_1.logger.error(errorMsg);
155
+ throw new Error(errorMsg);
156
+ }
157
+ // List mode
158
+ if (list) {
159
+ const skills = listSkills(sourcePath);
160
+ ui_1.logger.title('skills', 'Available Claude Code Skills:');
161
+ skills.forEach((skill) => {
162
+ ui_1.logger.log(` - ${skill}`);
163
+ });
164
+ ui_1.logger.log('');
165
+ ui_1.logger.info(`Total: ${skills.length} skills`);
166
+ return;
167
+ }
168
+ // Determine destination path
169
+ const destPath = project ? getProjectSkillsPath() : getPersonalSkillsPath();
170
+ // Check mode - compare versions without installing
171
+ if (check) {
172
+ const installedVersion = getInstalledVersion(destPath);
173
+ const packageVersion = getPackageVersion(sourcePath);
174
+ if (!installedVersion) {
175
+ ui_1.logger.warn('Skills are not installed.');
176
+ ui_1.logger.info(`Available version: ${packageVersion || 'unknown'}`);
177
+ ui_1.logger.info('Run `mbc install-skills` to install.');
178
+ return;
179
+ }
180
+ if (!packageVersion) {
181
+ ui_1.logger.warn('Could not determine package version.');
182
+ ui_1.logger.info(`Installed version: ${installedVersion}`);
183
+ return;
184
+ }
185
+ if (installedVersion === packageVersion) {
186
+ ui_1.logger.success(`Skills are up to date (${installedVersion}).`);
187
+ }
188
+ else {
189
+ ui_1.logger.warn(`Update available: ${installedVersion} → ${packageVersion}`);
190
+ ui_1.logger.info('Run `mbc install-skills --force` to update.');
191
+ }
192
+ return;
193
+ }
194
+ // Check if skills already exist
195
+ if ((0, fs_1.existsSync)(destPath) && !force) {
196
+ const existingSkills = (0, fs_1.readdirSync)(destPath, { withFileTypes: true })
197
+ .filter((entry) => entry.isDirectory())
198
+ .map((entry) => entry.name);
199
+ if (existingSkills.length > 0) {
200
+ ui_1.logger.warn(`Skills already exist at ${destPath}. Use --force to overwrite.`);
201
+ }
202
+ }
203
+ // Copy skills
204
+ ui_1.logger.title('install', `Installing skills to ${destPath}`);
205
+ const copiedSkills = copySkills(sourcePath, destPath);
206
+ // Write version file
207
+ const packageVersion = getPackageVersion(sourcePath);
208
+ if (packageVersion) {
209
+ writeVersionFile(destPath, packageVersion);
210
+ }
211
+ ui_1.logger.success(`Successfully installed ${copiedSkills.length} skills:`);
212
+ copiedSkills.forEach((skill) => {
213
+ ui_1.logger.log(` - ${skill}`);
214
+ });
215
+ if (packageVersion) {
216
+ ui_1.logger.log('');
217
+ ui_1.logger.info(`Version: ${packageVersion}`);
218
+ }
219
+ ui_1.logger.log('');
220
+ ui_1.logger.info('You can now use these skills in Claude Code:');
221
+ copiedSkills.forEach((skill) => {
222
+ ui_1.logger.log(` /${skill}`);
223
+ });
224
+ }
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const install_skills_action_1 = __importStar(require("./install-skills.action"));
40
+ jest.mock('fs');
41
+ jest.mock('os');
42
+ const fs_1 = require("fs");
43
+ const os_1 = __importDefault(require("os"));
44
+ const mockExistsSync = fs_1.existsSync;
45
+ const mockMkdirSync = fs_1.mkdirSync;
46
+ const mockCpSync = fs_1.cpSync;
47
+ const mockReaddirSync = fs_1.readdirSync;
48
+ const mockReadFileSync = fs_1.readFileSync;
49
+ const mockWriteFileSync = fs_1.writeFileSync;
50
+ describe('Install Skills Action', () => {
51
+ const mockCommand = {
52
+ name: () => 'install-skills',
53
+ opts: () => ({}),
54
+ };
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+ os_1.default.homedir.mockReturnValue('/home/user');
58
+ });
59
+ describe('getSkillsSourcePath', () => {
60
+ it('should return the path to mcp-server skills directory', () => {
61
+ // Use real path module, mock only existsSync to return true for skills path
62
+ mockExistsSync.mockReturnValue(true);
63
+ const sourcePath = (0, install_skills_action_1.getSkillsSourcePath)();
64
+ expect(sourcePath).toContain('skills');
65
+ });
66
+ it('should try multiple paths to find skills directory', () => {
67
+ // First call returns false, second returns true
68
+ mockExistsSync
69
+ .mockReturnValueOnce(false)
70
+ .mockReturnValueOnce(false)
71
+ .mockReturnValueOnce(true);
72
+ const sourcePath = (0, install_skills_action_1.getSkillsSourcePath)();
73
+ expect(sourcePath).toContain('skills');
74
+ expect(mockExistsSync).toHaveBeenCalledTimes(3);
75
+ });
76
+ it('should fallback to require.resolve when file paths not found', () => {
77
+ // All file paths return false, but require.resolve finds the package
78
+ mockExistsSync.mockReturnValue(false);
79
+ // In monorepo environment, require.resolve will find mcp-server
80
+ // This tests the fallback path (lines 62-64)
81
+ const sourcePath = (0, install_skills_action_1.getSkillsSourcePath)();
82
+ expect(sourcePath).toContain('skills');
83
+ });
84
+ });
85
+ describe('getPersonalSkillsPath', () => {
86
+ it('should return ~/.claude/skills/ path', () => {
87
+ const personalPath = (0, install_skills_action_1.getPersonalSkillsPath)();
88
+ expect(personalPath).toContain('/home/user');
89
+ expect(personalPath).toContain('.claude');
90
+ expect(personalPath).toContain('skills');
91
+ });
92
+ });
93
+ describe('getProjectSkillsPath', () => {
94
+ it('should return .claude/skills/ path in current directory', () => {
95
+ const projectPath = (0, install_skills_action_1.getProjectSkillsPath)();
96
+ expect(projectPath).toContain('.claude/skills');
97
+ });
98
+ });
99
+ describe('copySkills', () => {
100
+ const mockSourcePath = '/source/skills';
101
+ const mockDestPath = '/dest/skills';
102
+ beforeEach(() => {
103
+ mockReaddirSync.mockReturnValue([
104
+ { name: 'mbc-generate', isDirectory: () => true },
105
+ { name: 'mbc-review', isDirectory: () => true },
106
+ { name: 'mbc-migrate', isDirectory: () => true },
107
+ { name: 'mbc-debug', isDirectory: () => true },
108
+ ]);
109
+ });
110
+ it('should create destination directory if it does not exist', () => {
111
+ mockExistsSync.mockReturnValue(false);
112
+ (0, install_skills_action_1.copySkills)(mockSourcePath, mockDestPath);
113
+ expect(mockMkdirSync).toHaveBeenCalledWith(mockDestPath, {
114
+ recursive: true,
115
+ });
116
+ });
117
+ it('should not create destination directory if it exists', () => {
118
+ mockExistsSync.mockReturnValue(true);
119
+ (0, install_skills_action_1.copySkills)(mockSourcePath, mockDestPath);
120
+ expect(mockMkdirSync).not.toHaveBeenCalled();
121
+ });
122
+ it('should copy all skill directories', () => {
123
+ mockExistsSync.mockReturnValue(true);
124
+ (0, install_skills_action_1.copySkills)(mockSourcePath, mockDestPath);
125
+ expect(mockCpSync).toHaveBeenCalledTimes(4);
126
+ expect(mockCpSync).toHaveBeenCalledWith(expect.stringContaining('mbc-generate'), expect.stringContaining('mbc-generate'), { recursive: true });
127
+ expect(mockCpSync).toHaveBeenCalledWith(expect.stringContaining('mbc-review'), expect.stringContaining('mbc-review'), { recursive: true });
128
+ expect(mockCpSync).toHaveBeenCalledWith(expect.stringContaining('mbc-migrate'), expect.stringContaining('mbc-migrate'), { recursive: true });
129
+ expect(mockCpSync).toHaveBeenCalledWith(expect.stringContaining('mbc-debug'), expect.stringContaining('mbc-debug'), { recursive: true });
130
+ });
131
+ it('should return the list of copied skills', () => {
132
+ mockExistsSync.mockReturnValue(true);
133
+ const copiedSkills = (0, install_skills_action_1.copySkills)(mockSourcePath, mockDestPath);
134
+ expect(copiedSkills).toEqual([
135
+ 'mbc-generate',
136
+ 'mbc-review',
137
+ 'mbc-migrate',
138
+ 'mbc-debug',
139
+ ]);
140
+ });
141
+ });
142
+ describe('installSkillsAction', () => {
143
+ beforeEach(() => {
144
+ mockExistsSync.mockReturnValue(true);
145
+ mockReaddirSync.mockReturnValue([
146
+ { name: 'mbc-generate', isDirectory: () => true },
147
+ { name: 'mbc-review', isDirectory: () => true },
148
+ { name: 'mbc-migrate', isDirectory: () => true },
149
+ { name: 'mbc-debug', isDirectory: () => true },
150
+ ]);
151
+ });
152
+ describe('Personal installation (default)', () => {
153
+ it('should install skills to personal directory by default', async () => {
154
+ const options = {};
155
+ await (0, install_skills_action_1.default)(options, mockCommand);
156
+ expect(mockCpSync).toHaveBeenCalled();
157
+ });
158
+ it('should install to ~/.claude/skills/', async () => {
159
+ const options = {};
160
+ await (0, install_skills_action_1.default)(options, mockCommand);
161
+ expect(mockCpSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('/home/user/.claude/skills'), expect.any(Object));
162
+ });
163
+ });
164
+ describe('Project installation (--project flag)', () => {
165
+ it('should install skills to project directory when --project is specified', async () => {
166
+ const options = { project: true };
167
+ await (0, install_skills_action_1.default)(options, mockCommand);
168
+ expect(mockCpSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('.claude/skills'), expect.any(Object));
169
+ });
170
+ });
171
+ describe('Error handling', () => {
172
+ it('should handle missing source directory after finding path', async () => {
173
+ // Source path is found, but verification fails
174
+ mockExistsSync
175
+ .mockReturnValueOnce(true) // getSkillsSourcePath finds it
176
+ .mockReturnValueOnce(false); // but verification fails
177
+ await expect((0, install_skills_action_1.default)({}, mockCommand)).rejects.toThrow('Skills source directory not found');
178
+ });
179
+ it('should handle undefined command', async () => {
180
+ mockExistsSync.mockReturnValue(true);
181
+ await expect((0, install_skills_action_1.default)({}, undefined)).rejects.toThrow('Command is required');
182
+ });
183
+ });
184
+ describe('Force option', () => {
185
+ it('should overwrite existing skills when --force is specified', async () => {
186
+ const options = { force: true };
187
+ mockExistsSync.mockReturnValue(true);
188
+ await (0, install_skills_action_1.default)(options, mockCommand);
189
+ expect(mockCpSync).toHaveBeenCalled();
190
+ });
191
+ });
192
+ describe('List option', () => {
193
+ it('should list available skills when --list is specified', async () => {
194
+ const options = { list: true };
195
+ await (0, install_skills_action_1.default)(options, mockCommand);
196
+ // Should not copy when just listing
197
+ expect(mockCpSync).not.toHaveBeenCalled();
198
+ });
199
+ });
200
+ });
201
+ describe('Integration scenarios', () => {
202
+ beforeEach(() => {
203
+ mockExistsSync.mockReturnValue(true);
204
+ mockReaddirSync.mockReturnValue([
205
+ { name: 'mbc-generate', isDirectory: () => true },
206
+ { name: 'mbc-review', isDirectory: () => true },
207
+ { name: 'mbc-migrate', isDirectory: () => true },
208
+ { name: 'mbc-debug', isDirectory: () => true },
209
+ ]);
210
+ });
211
+ it('should handle installation to non-existent directory', async () => {
212
+ mockExistsSync.mockImplementation((p) => {
213
+ if (p.includes('mcp-server/skills'))
214
+ return true;
215
+ return false;
216
+ });
217
+ await (0, install_skills_action_1.default)({}, mockCommand);
218
+ expect(mockMkdirSync).toHaveBeenCalled();
219
+ });
220
+ it('should handle multiple concurrent installations', async () => {
221
+ const promises = [
222
+ (0, install_skills_action_1.default)({}, mockCommand),
223
+ (0, install_skills_action_1.default)({ project: true }, mockCommand),
224
+ ];
225
+ await expect(Promise.all(promises)).resolves.not.toThrow();
226
+ });
227
+ });
228
+ describe('Version management', () => {
229
+ describe('getInstalledVersion', () => {
230
+ it('should return null if version file does not exist', () => {
231
+ mockExistsSync.mockReturnValue(false);
232
+ const version = (0, install_skills_action_1.getInstalledVersion)('/dest/skills');
233
+ expect(version).toBeNull();
234
+ });
235
+ it('should return version from version file', () => {
236
+ mockExistsSync.mockReturnValue(true);
237
+ mockReadFileSync.mockReturnValue('1.0.24');
238
+ const version = (0, install_skills_action_1.getInstalledVersion)('/dest/skills');
239
+ expect(version).toBe('1.0.24');
240
+ });
241
+ it('should trim whitespace from version', () => {
242
+ mockExistsSync.mockReturnValue(true);
243
+ mockReadFileSync.mockReturnValue(' 1.0.24\n ');
244
+ const version = (0, install_skills_action_1.getInstalledVersion)('/dest/skills');
245
+ expect(version).toBe('1.0.24');
246
+ });
247
+ it('should return null on read error', () => {
248
+ mockExistsSync.mockReturnValue(true);
249
+ mockReadFileSync.mockImplementation(() => {
250
+ throw new Error('Read error');
251
+ });
252
+ const version = (0, install_skills_action_1.getInstalledVersion)('/dest/skills');
253
+ expect(version).toBeNull();
254
+ });
255
+ });
256
+ describe('getPackageVersion', () => {
257
+ it('should return version from package.json', () => {
258
+ mockExistsSync.mockReturnValue(true);
259
+ mockReadFileSync.mockReturnValue(JSON.stringify({ version: '1.0.25' }));
260
+ const version = (0, install_skills_action_1.getPackageVersion)('/source/skills');
261
+ expect(version).toBe('1.0.25');
262
+ });
263
+ it('should return null if package.json does not exist', () => {
264
+ mockExistsSync.mockReturnValue(false);
265
+ const version = (0, install_skills_action_1.getPackageVersion)('/source/skills');
266
+ expect(version).toBeNull();
267
+ });
268
+ it('should return null on parse error', () => {
269
+ mockExistsSync.mockReturnValue(true);
270
+ mockReadFileSync.mockReturnValue('invalid json');
271
+ const version = (0, install_skills_action_1.getPackageVersion)('/source/skills');
272
+ expect(version).toBeNull();
273
+ });
274
+ });
275
+ describe('writeVersionFile', () => {
276
+ it('should write version to file', () => {
277
+ (0, install_skills_action_1.writeVersionFile)('/dest/skills', '1.0.25');
278
+ expect(mockWriteFileSync).toHaveBeenCalledWith(expect.stringContaining(install_skills_action_1.VERSION_FILE_NAME), '1.0.25', 'utf-8');
279
+ });
280
+ });
281
+ describe('Check option', () => {
282
+ beforeEach(() => {
283
+ mockReaddirSync.mockReturnValue([
284
+ { name: 'mbc-generate', isDirectory: () => true },
285
+ { name: 'mbc-review', isDirectory: () => true },
286
+ ]);
287
+ });
288
+ it('should check for updates when --check is specified', async () => {
289
+ const options = { check: true };
290
+ mockExistsSync.mockReturnValue(true);
291
+ mockReadFileSync
292
+ .mockReturnValueOnce('1.0.24') // installed version
293
+ .mockReturnValueOnce(JSON.stringify({ version: '1.0.25' })); // package version
294
+ await (0, install_skills_action_1.default)(options, mockCommand);
295
+ // Should not copy when just checking
296
+ expect(mockCpSync).not.toHaveBeenCalled();
297
+ });
298
+ it('should report up-to-date when versions match', async () => {
299
+ const options = { check: true };
300
+ mockExistsSync.mockReturnValue(true);
301
+ mockReadFileSync
302
+ .mockReturnValueOnce('1.0.25') // installed version
303
+ .mockReturnValueOnce(JSON.stringify({ version: '1.0.25' })); // package version
304
+ await (0, install_skills_action_1.default)(options, mockCommand);
305
+ expect(mockCpSync).not.toHaveBeenCalled();
306
+ });
307
+ it('should report not installed when version file does not exist', async () => {
308
+ const options = { check: true };
309
+ // First call for source path check, second for version file check (false)
310
+ mockExistsSync.mockImplementation((p) => {
311
+ if (p.includes('mcp-server/skills') || p.includes('mcp-server\\skills')) {
312
+ return true;
313
+ }
314
+ if (p.includes(install_skills_action_1.VERSION_FILE_NAME)) {
315
+ return false;
316
+ }
317
+ return true;
318
+ });
319
+ mockReadFileSync.mockReturnValue(JSON.stringify({ version: '1.0.25' }));
320
+ await (0, install_skills_action_1.default)(options, mockCommand);
321
+ expect(mockCpSync).not.toHaveBeenCalled();
322
+ });
323
+ it('should handle unknown package version', async () => {
324
+ const options = { check: true };
325
+ mockExistsSync.mockImplementation((p) => {
326
+ // Package.json does not exist
327
+ if (p.includes('package.json')) {
328
+ return false;
329
+ }
330
+ return true;
331
+ });
332
+ mockReadFileSync.mockReturnValue('1.0.24'); // installed version only
333
+ await (0, install_skills_action_1.default)(options, mockCommand);
334
+ expect(mockCpSync).not.toHaveBeenCalled();
335
+ });
336
+ });
337
+ describe('Version file creation during installation', () => {
338
+ beforeEach(() => {
339
+ mockExistsSync.mockReturnValue(true);
340
+ mockReaddirSync.mockReturnValue([
341
+ { name: 'mbc-generate', isDirectory: () => true },
342
+ ]);
343
+ mockReadFileSync.mockReturnValue(JSON.stringify({ version: '1.0.25' }));
344
+ });
345
+ it('should create version file after installation', async () => {
346
+ await (0, install_skills_action_1.default)({}, mockCommand);
347
+ expect(mockWriteFileSync).toHaveBeenCalledWith(expect.stringContaining(install_skills_action_1.VERSION_FILE_NAME), '1.0.25', 'utf-8');
348
+ });
349
+ });
350
+ });
351
+ });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = loadCommands;
4
4
  const generate_command_1 = require("./generate.command");
5
+ const install_skills_command_1 = require("./install-skills.command");
5
6
  const new_command_1 = require("./new.command");
6
7
  const start_command_1 = require("./start.command");
7
8
  const ui_command_1 = require("./ui.command");
@@ -10,6 +11,7 @@ function loadCommands(program) {
10
11
  (0, start_command_1.startCommand)(program);
11
12
  (0, ui_command_1.uiCommand)(program);
12
13
  (0, generate_command_1.generateCommand)(program);
14
+ (0, install_skills_command_1.installSkillsCommand)(program);
13
15
  // error handling
14
16
  program.on('command:*', () => {
15
17
  console.error(`\nInvalid command: '${program.args.join(' ')}'`);
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installSkillsCommand = installSkillsCommand;
7
+ const install_skills_action_1 = __importDefault(require("../actions/install-skills.action"));
8
+ function installSkillsCommand(program) {
9
+ program
10
+ .command('install-skills')
11
+ .alias('skills')
12
+ .description('Install Claude Code skills for MBC CQRS Serverless development')
13
+ .option('-p, --project', 'Install to project directory (.claude/skills/) instead of personal (~/.claude/skills/)', false)
14
+ .option('-f, --force', 'Overwrite existing skills', false)
15
+ .option('-l, --list', 'List available skills without installing', false)
16
+ .option('-c, --check', 'Check if updates are available without installing', false)
17
+ .action(install_skills_action_1.default);
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mbc-cqrs-serverless/cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "a CLI to get started with MBC CQRS serverless framework",
5
5
  "keywords": [
6
6
  "mbc",
@@ -58,5 +58,5 @@
58
58
  "@faker-js/faker": "^8.3.1",
59
59
  "copyfiles": "^2.4.1"
60
60
  },
61
- "gitHead": "7d77028acefcce1a4a5b0fbeee2524d2dc264e64"
61
+ "gitHead": "1160a791f4cacaba65620f048611294737b7e2aa"
62
62
  }
@@ -1090,7 +1090,7 @@ export class InfraStack extends cdk.Stack {
1090
1090
  )
1091
1091
 
1092
1092
  const sesPolicy = new cdk.aws_iam.PolicyStatement({
1093
- actions: ['ses:SendEmail'],
1093
+ actions: ['ses:SendEmail', 'ses:SendTemplatedEmail'],
1094
1094
  resources: ['*'],
1095
1095
  })
1096
1096