mspec 0.0.9 → 0.0.10
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/dist/commands/init.js +47 -34
- package/dist/commands/init.test.js +55 -11
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -13,14 +13,17 @@ const { prompt } = require('enquirer');
|
|
|
13
13
|
async function initCommand() {
|
|
14
14
|
const mspecDir = path_1.default.join(process.cwd(), '.mspec');
|
|
15
15
|
const configPath = path_1.default.join(mspecDir, 'mspec.json');
|
|
16
|
-
let
|
|
16
|
+
let existingAgents = [];
|
|
17
|
+
let config = {};
|
|
17
18
|
if (fs_1.default.existsSync(mspecDir)) {
|
|
18
|
-
console.log(chalk_1.default.blue('Existing .mspec directory found. Updating integration files...'));
|
|
19
19
|
if (fs_1.default.existsSync(configPath)) {
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
if (config && config.
|
|
23
|
-
|
|
21
|
+
config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
22
|
+
if (config && config.agents && Array.isArray(config.agents)) {
|
|
23
|
+
existingAgents = config.agents;
|
|
24
|
+
}
|
|
25
|
+
else if (config && config.agent) {
|
|
26
|
+
existingAgents = [config.agent];
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
catch (e) {
|
|
@@ -28,22 +31,27 @@ async function initCommand() {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
const choices = ['claude', 'gemini', 'cursor', 'opencode', 'zed', 'generic'];
|
|
35
|
+
let selectedAgents = [];
|
|
36
|
+
try {
|
|
37
|
+
const response = await prompt({
|
|
38
|
+
type: 'multiselect',
|
|
39
|
+
name: 'agents',
|
|
40
|
+
message: 'Which AI agents would you like to configure? (Space to toggle, Enter to confirm)',
|
|
41
|
+
choices: choices.map(c => ({
|
|
42
|
+
name: c,
|
|
43
|
+
enabled: existingAgents.includes(c)
|
|
44
|
+
}))
|
|
45
|
+
});
|
|
46
|
+
selectedAgents = response.agents;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.log(chalk_1.default.yellow('\nInitialization cancelled.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!selectedAgents || selectedAgents.length === 0) {
|
|
53
|
+
console.log(chalk_1.default.yellow('No agents selected. Setup incomplete.'));
|
|
54
|
+
return;
|
|
47
55
|
}
|
|
48
56
|
// Create directories if they don't exist
|
|
49
57
|
if (!fs_1.default.existsSync(mspecDir)) {
|
|
@@ -52,25 +60,30 @@ async function initCommand() {
|
|
|
52
60
|
}
|
|
53
61
|
// Write mspec.json config
|
|
54
62
|
const mspecConfig = {
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
...config,
|
|
64
|
+
agents: selectedAgents,
|
|
65
|
+
paths: config.paths || {
|
|
57
66
|
specs: '.mspec/specs',
|
|
58
67
|
tasks: '.mspec/tasks'
|
|
59
68
|
}
|
|
60
69
|
};
|
|
70
|
+
// Remove legacy field
|
|
71
|
+
delete mspecConfig.agent;
|
|
61
72
|
fs_1.default.writeFileSync(configPath, JSON.stringify(mspecConfig, null, 2));
|
|
62
|
-
// Write agent integration files
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// Write agent integration files for each selected agent
|
|
74
|
+
for (const agent of selectedAgents) {
|
|
75
|
+
const agentTemplates = (0, templates_1.getTemplates)(agent);
|
|
76
|
+
if (agentTemplates.length > 0) {
|
|
77
|
+
for (const template of agentTemplates) {
|
|
78
|
+
const targetDir = path_1.default.join(process.cwd(), template.dir);
|
|
79
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, template.file), template.content);
|
|
81
|
+
console.log(chalk_1.default.green(`Updated integration file for ${agent} at ${path_1.default.join(template.dir, template.file)}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log(chalk_1.default.yellow(`No specific integration template found for ${agent}. Setup completed with generic settings.`));
|
|
70
86
|
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
console.log(chalk_1.default.yellow(`No specific integration template found for ${agent}. Setup completed with generic settings.`));
|
|
74
87
|
}
|
|
75
88
|
console.log(chalk_1.default.green('mspec initialized/updated successfully!'));
|
|
76
89
|
}
|
|
@@ -37,7 +37,7 @@ describe('initCommand', () => {
|
|
|
37
37
|
];
|
|
38
38
|
providers.forEach(({ agent, expectedFiles }) => {
|
|
39
39
|
it(`should initialize the .mspec environment for ${agent}`, async () => {
|
|
40
|
-
mockPrompt.mockResolvedValueOnce({ agent });
|
|
40
|
+
mockPrompt.mockResolvedValueOnce({ agents: [agent] });
|
|
41
41
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
42
42
|
await (0, init_1.initCommand)();
|
|
43
43
|
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec'))).toBe(true);
|
|
@@ -46,33 +46,77 @@ describe('initCommand', () => {
|
|
|
46
46
|
const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
|
|
47
47
|
expect(fs_1.default.existsSync(configPath)).toBe(true);
|
|
48
48
|
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
49
|
-
expect(config.
|
|
49
|
+
expect(config.agents).toContain(agent);
|
|
50
50
|
for (const expectedFile of expectedFiles) {
|
|
51
51
|
const integrationPath = path_1.default.join(tmpDir, expectedFile);
|
|
52
52
|
expect(fs_1.default.existsSync(integrationPath)).toBe(true);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
-
it('should
|
|
56
|
+
it('should initialize multiple agents at once', async () => {
|
|
57
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['claude', 'gemini'] });
|
|
58
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
59
|
+
await (0, init_1.initCommand)();
|
|
60
|
+
const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
|
|
61
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
62
|
+
expect(config.agents).toEqual(['claude', 'gemini']);
|
|
63
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.claude/commands/mspec.spec.md'))).toBe(true);
|
|
64
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.gemini/commands/mspec.spec.toml'))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('should update and prompt even if .mspec directory already exists', async () => {
|
|
57
67
|
const mspecDir = path_1.default.join(tmpDir, '.mspec');
|
|
58
68
|
fs_1.default.mkdirSync(mspecDir);
|
|
59
69
|
fs_1.default.mkdirSync(path_1.default.join(mspecDir, 'specs'));
|
|
60
70
|
fs_1.default.mkdirSync(path_1.default.join(mspecDir, 'tasks'));
|
|
61
|
-
fs_1.default.writeFileSync(path_1.default.join(mspecDir, 'mspec.json'), JSON.stringify({
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(mspecDir, 'mspec.json'), JSON.stringify({ agents: ['cursor'] }));
|
|
62
72
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
63
|
-
mockPrompt.
|
|
73
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['cursor', 'gemini'] });
|
|
64
74
|
await (0, init_1.initCommand)();
|
|
65
|
-
|
|
75
|
+
const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
|
|
76
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
77
|
+
expect(config.agents).toEqual(['cursor', 'gemini']);
|
|
66
78
|
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.cursor/rules/mspec.spec.mdc'))).toBe(true);
|
|
79
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.gemini/commands/mspec.spec.toml'))).toBe(true);
|
|
67
80
|
});
|
|
68
|
-
it('should
|
|
81
|
+
it('should handle legacy "agent" field and migrate to "agents"', async () => {
|
|
69
82
|
const mspecDir = path_1.default.join(tmpDir, '.mspec');
|
|
70
83
|
fs_1.default.mkdirSync(mspecDir);
|
|
71
|
-
|
|
84
|
+
fs_1.default.writeFileSync(path_1.default.join(mspecDir, 'mspec.json'), JSON.stringify({ agent: 'claude' }));
|
|
85
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['claude', 'gemini'] });
|
|
86
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
87
|
+
await (0, init_1.initCommand)();
|
|
88
|
+
const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
|
|
89
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
90
|
+
expect(config.agents).toEqual(['claude', 'gemini']);
|
|
91
|
+
expect(config.agent).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
it('should support running init multiple times to add/remove agents', async () => {
|
|
72
94
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
95
|
+
// 1st init: Select 'claude'
|
|
96
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['claude'] });
|
|
73
97
|
await (0, init_1.initCommand)();
|
|
74
|
-
|
|
98
|
+
let config = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.mspec/mspec.json'), 'utf-8'));
|
|
99
|
+
expect(config.agents).toEqual(['claude']);
|
|
100
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.claude/commands/mspec.spec.md'))).toBe(true);
|
|
101
|
+
// 2nd init: Add 'gemini', 'claude' should be pre-selected (enabled: true)
|
|
102
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['claude', 'gemini'] });
|
|
103
|
+
await (0, init_1.initCommand)();
|
|
104
|
+
// Verify prompt was called with 'claude' enabled
|
|
105
|
+
expect(mockPrompt).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
106
|
+
choices: expect.arrayContaining([
|
|
107
|
+
expect.objectContaining({ name: 'claude', enabled: true }),
|
|
108
|
+
expect.objectContaining({ name: 'gemini', enabled: false })
|
|
109
|
+
])
|
|
110
|
+
}));
|
|
111
|
+
config = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.mspec/mspec.json'), 'utf-8'));
|
|
112
|
+
expect(config.agents).toEqual(['claude', 'gemini']);
|
|
75
113
|
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.gemini/commands/mspec.spec.toml'))).toBe(true);
|
|
114
|
+
// 3rd init: Remove 'claude', keep 'gemini'
|
|
115
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['gemini'] });
|
|
116
|
+
await (0, init_1.initCommand)();
|
|
117
|
+
config = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.mspec/mspec.json'), 'utf-8'));
|
|
118
|
+
expect(config.agents).toEqual(['gemini']);
|
|
119
|
+
// Note: mspec currently doesn't delete files of removed agents, which is fine for now
|
|
76
120
|
});
|
|
77
121
|
it('should handle prompt cancellation gracefully', async () => {
|
|
78
122
|
mockPrompt.mockRejectedValueOnce(new Error('User cancelled'));
|
|
@@ -82,13 +126,13 @@ describe('initCommand', () => {
|
|
|
82
126
|
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec'))).toBe(false);
|
|
83
127
|
});
|
|
84
128
|
it('should handle missing template gracefully', async () => {
|
|
85
|
-
mockPrompt.mockResolvedValueOnce({
|
|
129
|
+
mockPrompt.mockResolvedValueOnce({ agents: ['unknown-agent'] });
|
|
86
130
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
87
131
|
await (0, init_1.initCommand)();
|
|
88
132
|
const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
|
|
89
133
|
expect(fs_1.default.existsSync(configPath)).toBe(true);
|
|
90
134
|
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
91
|
-
expect(config.
|
|
135
|
+
expect(config.agents).toContain('unknown-agent');
|
|
92
136
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No specific integration template found'));
|
|
93
137
|
});
|
|
94
138
|
});
|