mspec 0.0.1

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 ADDED
@@ -0,0 +1,94 @@
1
+ # mspec
2
+
3
+ > **A minimalist Spec-Driven Development (SDD) toolkit for solo developers and AI agents.**
4
+
5
+ `mspec` is a lightweight alternative to heavy SDD frameworks. It removes the "enterprise theater" (branch-per-feature, complex state files, and heavy daemon processes) and focuses strictly on **intent** (the Spec) and **execution** (the Tasks) using simple Markdown files.
6
+
7
+ It is designed to work seamlessly alongside your favorite AI coding agents: Claude Code, Gemini CLI, Cursor, OpenCode, or Zed.
8
+
9
+ ## Philosophy
10
+ - **Token Efficient:** Uses a single `SPEC.md` for context instead of massive chat histories.
11
+ - **Visual-First:** Encourages Mermaid.js diagrams over long, confusing paragraphs.
12
+ - **Data Dictionaries:** Uses simple Markdown tables for data modeling instead of strict, unreadable JSON schemas.
13
+ - **Agent Handoff:** You design the spec; the AI breaks down the tasks; the AI implements the tasks sequentially.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ You can run `mspec` directly via `npx` (recommended) or install it globally.
20
+
21
+ ### Running via npx (No install required)
22
+ ```bash
23
+ npx mspec <command>
24
+ ```
25
+
26
+ ### Global Installation
27
+ ```bash
28
+ npm install -g mspec
29
+ ```
30
+
31
+ *(Note: If you are cloning this repository locally for development, run `npm install`, then `npm run build`, and finally `npm link` to make the `mspec` command available globally on your machine).*
32
+
33
+ ---
34
+
35
+ ## How to Use
36
+
37
+ The workflow follows a simple three-step loop: **Initialize -> Plan -> Implement**.
38
+
39
+ ### Step 1: Initialize the Project
40
+ Run this command in the root of your project:
41
+ ```bash
42
+ npx mspec init
43
+ ```
44
+ - It will prompt you for your preferred AI agent (Claude, Gemini, Cursor, etc.).
45
+ - It will create the `.mspec/specs/` and `.mspec/tasks/` directories.
46
+ - It will automatically inject a custom command/instruction file into your project (e.g., `.gemini/commands/mspec.toml` or `.cursor/rules/mspec.mdc`) so your AI agent understands the framework.
47
+
48
+ ### Step 2: The Inquiry (Creating a Spec)
49
+ Talk to your AI agent and ask it to draft a specification using the `mspec` standard.
50
+
51
+ **Example Prompt to your AI:**
52
+ > "Let's create a spec for a new authentication feature. Save it to `.mspec/specs/001-auth.md`. Include a markdown description, a mermaid sequence diagram for login, and a markdown data dictionary for the user object."
53
+
54
+ ### Step 3: Scaffold the Plan
55
+ Once you are happy with the `001-auth.md` spec, run:
56
+ ```bash
57
+ npx mspec plan 001-auth
58
+ ```
59
+ - This validates that the spec exists.
60
+ - It generates a boilerplate `.mspec/tasks/001-auth.tasks.md` file containing strict instructions for the AI.
61
+ - **Next Action:** Tell your AI agent: *"Please read the spec and fill out the tasks file for 001-auth."* The AI will break the spec down into granular checkboxes.
62
+
63
+ ### Step 4: Implement and Execute
64
+ Once the checklist is generated, it's time to hand the wheel over to the AI.
65
+ ```bash
66
+ npx mspec implement 001-auth
67
+ ```
68
+ - This command analyzes the task list.
69
+ - It outputs a highly structured prompt to your terminal.
70
+ - **Next Action:** Copy the outputted prompt and paste it to your AI agent.
71
+ - *What the AI does:* It will read the `.tasks.md` file, pick the first unchecked `- [ ]` task, implement the code, run your tests, change the checkbox to `- [x]`, and then **stop** to wait for your review.
72
+
73
+ #### Batch Execution
74
+ If you trust the AI and want it to burn through the whole checklist without stopping for your approval between tasks, use the `--batch` flag:
75
+ ```bash
76
+ npx mspec implement 001-auth --batch
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Directory Structure
82
+ A project using `mspec` will look like this:
83
+
84
+ ```text
85
+ your-project/
86
+ ├── .mspec/
87
+ │ ├── mspec.json # Auto-generated config
88
+ │ ├── specs/
89
+ │ │ └── 001-auth.md # The "Intent" (Markdown/Mermaid)
90
+ │ └── tasks/
91
+ │ └── 001-auth.tasks.md # The "Execution" (Checklists)
92
+ ├── src/ # Your actual code
93
+ └── package.json
94
+ ```
@@ -0,0 +1,47 @@
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.implementCommand = implementCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ async function implementCommand(specName, options) {
11
+ if (typeof specName !== 'string' || specName.includes('..') || path_1.default.isAbsolute(specName)) {
12
+ console.error(chalk_1.default.red(`Error: Invalid spec name. Path traversal is not allowed.`));
13
+ process.exit(1);
14
+ return;
15
+ }
16
+ const mspecDir = path_1.default.join(process.cwd(), '.mspec');
17
+ const tasksPath = path_1.default.join(mspecDir, 'tasks', `${specName}.tasks.md`);
18
+ // 1. Check if the tasks file exists
19
+ if (!fs_1.default.existsSync(tasksPath)) {
20
+ console.error(chalk_1.default.red(`Error: Tasks file not found at ${tasksPath}`));
21
+ console.log(chalk_1.default.yellow(`Run 'mspec plan ${specName}' and have your AI fill it out first.`));
22
+ process.exit(1);
23
+ return;
24
+ }
25
+ // 2. Read the file and check for remaining tasks
26
+ const tasksContent = fs_1.default.readFileSync(tasksPath, 'utf-8');
27
+ if (!tasksContent.includes('- [ ]')) {
28
+ console.log(chalk_1.default.yellow(`Warning: No incomplete tasks found in ${specName}.tasks.md.`));
29
+ console.log(chalk_1.default.green(`It looks like this spec is already fully implemented!`));
30
+ return;
31
+ }
32
+ // 3. Generate the execution prompt
33
+ const isBatch = options.batch === true;
34
+ const executionPrompt = `> **mspec execution directive:**
35
+ > Please read \`.mspec/tasks/${specName}.tasks.md\`.
36
+ > 1. Find the first incomplete task marked with \`- [ ]\`.
37
+ > 2. Implement the requirements for that specific task.
38
+ > 3. Verify your implementation (run tests/build).
39
+ > 4. If successful, change the task to \`- [x]\` in the file.
40
+ > 5. ${isBatch
41
+ ? 'Continue to the next task until all tasks in the current phase are complete.'
42
+ : 'Stop and wait for my approval before moving to the next task.'}`;
43
+ // 4. Output the prompt
44
+ console.log(chalk_1.default.magenta('\n=== Copy the text below and paste it to your AI agent ===\n'));
45
+ console.log(executionPrompt);
46
+ console.log(chalk_1.default.magenta('\n==========================================================\n'));
47
+ }
@@ -0,0 +1,92 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const implement_1 = require("./implement");
10
+ describe('implementCommand', () => {
11
+ let originalCwd;
12
+ let tmpDir;
13
+ let mockExit;
14
+ let mockConsoleError;
15
+ let mockConsoleLog;
16
+ beforeAll(() => {
17
+ originalCwd = process.cwd;
18
+ });
19
+ beforeEach(() => {
20
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'mspec-test-implement-'));
21
+ process.cwd = () => tmpDir;
22
+ mockExit = jest.spyOn(process, 'exit').mockImplementation((() => { }));
23
+ mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
24
+ mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
25
+ });
26
+ afterEach(() => {
27
+ process.cwd = originalCwd;
28
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
29
+ jest.restoreAllMocks();
30
+ });
31
+ it('should reject path traversal in spec name', async () => {
32
+ await (0, implement_1.implementCommand)('../outside-spec', {});
33
+ expect(mockExit).toHaveBeenCalledWith(1);
34
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid spec name'));
35
+ });
36
+ it('should reject absolute paths in spec name', async () => {
37
+ await (0, implement_1.implementCommand)('/etc/passwd', {});
38
+ expect(mockExit).toHaveBeenCalledWith(1);
39
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid spec name'));
40
+ });
41
+ it('should exit with error if tasks file does not exist', async () => {
42
+ await (0, implement_1.implementCommand)('missing-spec', {});
43
+ expect(mockExit).toHaveBeenCalledWith(1);
44
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Error: Tasks file not found'));
45
+ });
46
+ it('should warn if no incomplete tasks are found', async () => {
47
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks');
48
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
49
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'done-spec.tasks.md'), '- [x] task 1\n- [x] task 2');
50
+ await (0, implement_1.implementCommand)('done-spec', {});
51
+ expect(mockExit).not.toHaveBeenCalled();
52
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Warning: No incomplete tasks found'));
53
+ });
54
+ it('should handle empty task files gracefully', async () => {
55
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks');
56
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
57
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'empty.tasks.md'), '');
58
+ await (0, implement_1.implementCommand)('empty', {});
59
+ expect(mockExit).not.toHaveBeenCalled();
60
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Warning: No incomplete tasks found'));
61
+ });
62
+ it('should generate one-by-one execution prompt by default', async () => {
63
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks');
64
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
65
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'active-spec.tasks.md'), '- [x] task 1\n- [ ] task 2');
66
+ await (0, implement_1.implementCommand)('active-spec', {});
67
+ expect(mockExit).not.toHaveBeenCalled();
68
+ const promptOutput = mockConsoleLog.mock.calls[1][0];
69
+ expect(promptOutput).toContain('mspec execution directive:');
70
+ expect(promptOutput).toContain('active-spec.tasks.md');
71
+ expect(promptOutput).toContain('Stop and wait for my approval before moving to the next task');
72
+ });
73
+ it('should generate batch execution prompt when --batch is true', async () => {
74
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks');
75
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
76
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'batch-spec.tasks.md'), '- [ ] task 1\n- [ ] task 2');
77
+ await (0, implement_1.implementCommand)('batch-spec', { batch: true });
78
+ expect(mockExit).not.toHaveBeenCalled();
79
+ const promptOutput = mockConsoleLog.mock.calls[1][0];
80
+ expect(promptOutput).toContain('mspec execution directive:');
81
+ expect(promptOutput).toContain('Continue to the next task until all tasks');
82
+ });
83
+ it('should support nested spec names like feature/001-auth', async () => {
84
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks', 'feature');
85
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
86
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, '001-auth.tasks.md'), '- [ ] task 1');
87
+ await (0, implement_1.implementCommand)('feature/001-auth', {});
88
+ expect(mockExit).not.toHaveBeenCalled();
89
+ const promptOutput = mockConsoleLog.mock.calls[1][0];
90
+ expect(promptOutput).toContain('feature/001-auth.tasks.md');
91
+ });
92
+ });
@@ -0,0 +1,57 @@
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.initCommand = initCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const templates_1 = require("../templates");
11
+ // Use require for enquirer to avoid commonjs/esm interop issues with its types
12
+ const { prompt } = require('enquirer');
13
+ async function initCommand() {
14
+ const mspecDir = path_1.default.join(process.cwd(), '.mspec');
15
+ if (fs_1.default.existsSync(mspecDir)) {
16
+ console.log(chalk_1.default.yellow('.mspec directory already exists. Initialization skipped.'));
17
+ return;
18
+ }
19
+ let response;
20
+ try {
21
+ response = await prompt({
22
+ type: 'select',
23
+ name: 'agent',
24
+ message: 'Which AI agent are you using?',
25
+ choices: ['claude', 'gemini', 'cursor', 'opencode', 'zed', 'generic'],
26
+ });
27
+ }
28
+ catch (error) {
29
+ console.log(chalk_1.default.yellow('\nInitialization cancelled.'));
30
+ return;
31
+ }
32
+ const agent = response.agent;
33
+ // Create directories
34
+ fs_1.default.mkdirSync(path_1.default.join(mspecDir, 'specs'), { recursive: true });
35
+ fs_1.default.mkdirSync(path_1.default.join(mspecDir, 'tasks'), { recursive: true });
36
+ // Write mspec.json config
37
+ const mspecConfig = {
38
+ agent,
39
+ paths: {
40
+ specs: '.mspec/specs',
41
+ tasks: '.mspec/tasks'
42
+ }
43
+ };
44
+ fs_1.default.writeFileSync(path_1.default.join(mspecDir, 'mspec.json'), JSON.stringify(mspecConfig, null, 2));
45
+ // Write agent integration file
46
+ const template = (0, templates_1.getTemplate)(agent);
47
+ if (template) {
48
+ const targetDir = path_1.default.join(process.cwd(), template.dir);
49
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
50
+ fs_1.default.writeFileSync(path_1.default.join(targetDir, template.file), template.content);
51
+ console.log(chalk_1.default.green(`Created integration file for ${agent} at ${path_1.default.join(template.dir, template.file)}`));
52
+ }
53
+ else {
54
+ console.log(chalk_1.default.yellow(`No specific integration template found for ${agent}. Setup completed with generic settings.`));
55
+ }
56
+ console.log(chalk_1.default.green('mspec initialized successfully!'));
57
+ }
@@ -0,0 +1,79 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const init_1 = require("./init");
10
+ const mockPrompt = jest.fn();
11
+ jest.mock('enquirer', () => ({
12
+ prompt: (...args) => mockPrompt(...args)
13
+ }));
14
+ describe('initCommand', () => {
15
+ let originalCwd;
16
+ let tmpDir;
17
+ beforeAll(() => {
18
+ originalCwd = process.cwd;
19
+ });
20
+ beforeEach(() => {
21
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'mspec-test-init-'));
22
+ process.cwd = () => tmpDir;
23
+ mockPrompt.mockReset();
24
+ });
25
+ afterEach(() => {
26
+ process.cwd = originalCwd;
27
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
28
+ jest.restoreAllMocks();
29
+ });
30
+ const providers = [
31
+ { agent: 'claude', expectedFile: '.claude/commands/mspec.md' },
32
+ { agent: 'gemini', expectedFile: '.gemini/commands/mspec.toml' },
33
+ { agent: 'cursor', expectedFile: '.cursor/rules/mspec.mdc' },
34
+ { agent: 'opencode', expectedFile: '.opencode/commands/mspec.md' },
35
+ { agent: 'zed', expectedFile: '.mspec/INSTRUCTIONS.md' },
36
+ { agent: 'generic', expectedFile: '.mspec/INSTRUCTIONS.md' }
37
+ ];
38
+ providers.forEach(({ agent, expectedFile }) => {
39
+ it(`should initialize the .mspec environment for ${agent}`, async () => {
40
+ mockPrompt.mockResolvedValueOnce({ agent });
41
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
42
+ await (0, init_1.initCommand)();
43
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec'))).toBe(true);
44
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec/specs'))).toBe(true);
45
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec/tasks'))).toBe(true);
46
+ const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
47
+ expect(fs_1.default.existsSync(configPath)).toBe(true);
48
+ const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
49
+ expect(config.agent).toBe(agent);
50
+ const integrationPath = path_1.default.join(tmpDir, expectedFile);
51
+ expect(fs_1.default.existsSync(integrationPath)).toBe(true);
52
+ });
53
+ });
54
+ it('should skip initialization if .mspec directory already exists', async () => {
55
+ const mspecDir = path_1.default.join(tmpDir, '.mspec');
56
+ fs_1.default.mkdirSync(mspecDir);
57
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
58
+ await (0, init_1.initCommand)();
59
+ expect(fs_1.default.existsSync(path_1.default.join(mspecDir, 'specs'))).toBe(false);
60
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Initialization skipped.'));
61
+ });
62
+ it('should handle prompt cancellation gracefully', async () => {
63
+ mockPrompt.mockRejectedValueOnce(new Error('User cancelled'));
64
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
65
+ await (0, init_1.initCommand)();
66
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Initialization cancelled.'));
67
+ expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.mspec'))).toBe(false);
68
+ });
69
+ it('should handle missing template gracefully', async () => {
70
+ mockPrompt.mockResolvedValueOnce({ agent: 'unknown-agent' });
71
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
72
+ await (0, init_1.initCommand)();
73
+ const configPath = path_1.default.join(tmpDir, '.mspec/mspec.json');
74
+ expect(fs_1.default.existsSync(configPath)).toBe(true);
75
+ const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
76
+ expect(config.agent).toBe('unknown-agent');
77
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No specific integration template found'));
78
+ });
79
+ });
@@ -0,0 +1,52 @@
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.planCommand = planCommand;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ async function planCommand(specName) {
11
+ if (typeof specName !== 'string' || specName.includes('..') || path_1.default.isAbsolute(specName)) {
12
+ console.error(chalk_1.default.red(`Error: Invalid spec name. Path traversal is not allowed.`));
13
+ process.exit(1);
14
+ return;
15
+ }
16
+ const mspecDir = path_1.default.join(process.cwd(), '.mspec');
17
+ const specPath = path_1.default.join(mspecDir, 'specs', `${specName}.md`);
18
+ const tasksPath = path_1.default.join(mspecDir, 'tasks', `${specName}.tasks.md`);
19
+ // 1. Check if the spec file exists
20
+ if (!fs_1.default.existsSync(specPath)) {
21
+ console.error(chalk_1.default.red(`Error: Spec file not found at ${specPath}`));
22
+ console.log(chalk_1.default.yellow(`Make sure you have created the spec first using your AI agent or manually.`));
23
+ process.exit(1);
24
+ return;
25
+ }
26
+ // 2. Check if the tasks file already exists
27
+ if (fs_1.default.existsSync(tasksPath)) {
28
+ console.log(chalk_1.default.yellow(`Warning: Tasks file already exists at ${tasksPath}`));
29
+ console.log(chalk_1.default.gray(`Skipping generation to prevent overwriting your existing plan.`));
30
+ return;
31
+ }
32
+ // 3. Generate the boilerplate markdown
33
+ const boilerplate = `# Implementation Tasks: ${specName}
34
+
35
+ > **AI INSTRUCTION:** Read \`.mspec/specs/${specName}.md\`. Break down the requirements into granular, sequential implementation tasks below. Use checkboxes (\`- [ ]\`). Group by phases.
36
+
37
+ ## Phase 1: Setup & Scaffolding
38
+ - [ ] ...
39
+
40
+ ## Phase 2: Core Logic
41
+ - [ ] ...
42
+
43
+ ## Phase 3: Validation
44
+ - [ ] ...
45
+ `;
46
+ // Ensure tasks directory exists in case it was somehow deleted or it's a nested spec (e.g. 'auth/login')
47
+ fs_1.default.mkdirSync(path_1.default.dirname(tasksPath), { recursive: true });
48
+ // 4. Write the file
49
+ fs_1.default.writeFileSync(tasksPath, boilerplate, 'utf-8');
50
+ console.log(chalk_1.default.green(`Success: Scaffolded tasks file at ${tasksPath}`));
51
+ console.log(chalk_1.default.blue(`Next Step: Ask your AI agent to "fill out the tasks for ${specName}"`));
52
+ }
@@ -0,0 +1,78 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const plan_1 = require("./plan");
10
+ describe('planCommand', () => {
11
+ let originalCwd;
12
+ let tmpDir;
13
+ let mockExit;
14
+ let mockConsoleError;
15
+ let mockConsoleLog;
16
+ beforeAll(() => {
17
+ originalCwd = process.cwd;
18
+ });
19
+ beforeEach(() => {
20
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'mspec-test-plan-'));
21
+ process.cwd = () => tmpDir;
22
+ mockExit = jest.spyOn(process, 'exit').mockImplementation((() => { }));
23
+ mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => { });
24
+ mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => { });
25
+ });
26
+ afterEach(() => {
27
+ process.cwd = originalCwd;
28
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
29
+ jest.restoreAllMocks();
30
+ });
31
+ it('should reject path traversal in spec name', async () => {
32
+ await (0, plan_1.planCommand)('../outside-spec');
33
+ expect(mockExit).toHaveBeenCalledWith(1);
34
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid spec name'));
35
+ });
36
+ it('should reject absolute paths in spec name', async () => {
37
+ await (0, plan_1.planCommand)('/etc/passwd');
38
+ expect(mockExit).toHaveBeenCalledWith(1);
39
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Invalid spec name'));
40
+ });
41
+ it('should exit with error if spec file does not exist', async () => {
42
+ await (0, plan_1.planCommand)('missing-spec');
43
+ expect(mockExit).toHaveBeenCalledWith(1);
44
+ expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Error: Spec file not found'));
45
+ });
46
+ it('should skip generation if tasks file already exists', async () => {
47
+ const specsDir = path_1.default.join(tmpDir, '.mspec', 'specs');
48
+ const tasksDir = path_1.default.join(tmpDir, '.mspec', 'tasks');
49
+ fs_1.default.mkdirSync(specsDir, { recursive: true });
50
+ fs_1.default.mkdirSync(tasksDir, { recursive: true });
51
+ fs_1.default.writeFileSync(path_1.default.join(specsDir, 'test-spec.md'), '# Spec');
52
+ fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'test-spec.tasks.md'), '# Existing Tasks');
53
+ await (0, plan_1.planCommand)('test-spec');
54
+ expect(mockExit).not.toHaveBeenCalled();
55
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Warning: Tasks file already exists'));
56
+ const content = fs_1.default.readFileSync(path_1.default.join(tasksDir, 'test-spec.tasks.md'), 'utf-8');
57
+ expect(content).toBe('# Existing Tasks');
58
+ });
59
+ it('should scaffold boilerplate if spec exists and tasks do not', async () => {
60
+ const specsDir = path_1.default.join(tmpDir, '.mspec', 'specs');
61
+ fs_1.default.mkdirSync(specsDir, { recursive: true });
62
+ fs_1.default.writeFileSync(path_1.default.join(specsDir, 'test-spec.md'), '# Spec');
63
+ await (0, plan_1.planCommand)('test-spec');
64
+ expect(mockExit).not.toHaveBeenCalled();
65
+ expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Success: Scaffolded tasks file'));
66
+ const tasksPath = path_1.default.join(tmpDir, '.mspec', 'tasks', 'test-spec.tasks.md');
67
+ expect(fs_1.default.existsSync(tasksPath)).toBe(true);
68
+ });
69
+ it('should support nested spec names like feature/001-auth', async () => {
70
+ const specsDir = path_1.default.join(tmpDir, '.mspec', 'specs', 'feature');
71
+ fs_1.default.mkdirSync(specsDir, { recursive: true });
72
+ fs_1.default.writeFileSync(path_1.default.join(specsDir, '001-auth.md'), '# Spec');
73
+ await (0, plan_1.planCommand)('feature/001-auth');
74
+ expect(mockExit).not.toHaveBeenCalled();
75
+ const tasksPath = path_1.default.join(tmpDir, '.mspec', 'tasks', 'feature', '001-auth.tasks.md');
76
+ expect(fs_1.default.existsSync(tasksPath)).toBe(true);
77
+ });
78
+ });
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
5
+ const init_1 = require("./commands/init");
6
+ const plan_1 = require("./commands/plan");
7
+ const implement_1 = require("./commands/implement");
8
+ const program = new commander_1.Command();
9
+ program
10
+ .name('mspec')
11
+ .description('Minimalist Spec-Driven Development CLI')
12
+ .version('1.0.0');
13
+ program
14
+ .command('init')
15
+ .description('Initialize mspec in the current directory')
16
+ .action(init_1.initCommand);
17
+ program
18
+ .command('plan <spec-name>')
19
+ .description('Scaffold a tasks file for a given spec')
20
+ .action(plan_1.planCommand);
21
+ program
22
+ .command('implement <spec-name>')
23
+ .description('Generate execution instructions for the AI agent to implement a spec')
24
+ .option('-b, --batch', 'Instruct the AI to complete all tasks without stopping for approval', false)
25
+ .action(implement_1.implementCommand);
26
+ program.parse(process.argv);
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.templates = void 0;
4
+ exports.getTemplate = getTemplate;
5
+ exports.templates = {
6
+ claude: {
7
+ dir: '.claude/commands',
8
+ file: 'mspec.md',
9
+ content: `---
10
+ description: "Commands for Spec-Driven Development using mspec"
11
+ ---
12
+ You are an AI assistant using the mspec framework.
13
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
14
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
15
+ `
16
+ },
17
+ gemini: {
18
+ dir: '.gemini/commands',
19
+ file: 'mspec.toml',
20
+ content: `description = "Commands for Spec-Driven Development using mspec"
21
+ prompt = """
22
+ You are an AI assistant using the mspec framework.
23
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
24
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
25
+ """
26
+ `
27
+ },
28
+ cursor: {
29
+ dir: '.cursor/rules',
30
+ file: 'mspec.mdc',
31
+ content: `---
32
+ description: Commands for Spec-Driven Development using mspec
33
+ globs: *
34
+ ---
35
+ You are an AI assistant using the mspec framework.
36
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
37
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
38
+ `
39
+ },
40
+ opencode: {
41
+ dir: '.opencode/commands',
42
+ file: 'mspec.md',
43
+ content: `---
44
+ description: "Commands for Spec-Driven Development using mspec"
45
+ ---
46
+ You are an AI assistant using the mspec framework.
47
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
48
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
49
+ `
50
+ },
51
+ zed: {
52
+ dir: '.mspec',
53
+ file: 'INSTRUCTIONS.md',
54
+ content: `# mspec Instructions
55
+
56
+ You are an AI assistant using the mspec framework.
57
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
58
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
59
+ `
60
+ },
61
+ generic: {
62
+ dir: '.mspec',
63
+ file: 'INSTRUCTIONS.md',
64
+ content: `# mspec Instructions
65
+
66
+ You are an AI assistant using the mspec framework.
67
+ When asked to /mspec:spec, /mspec:plan, or /mspec:apply, follow the minimalist spec-driven development guidelines.
68
+ Specs are in .mspec/specs. Tasks are in .mspec/tasks.
69
+ `
70
+ }
71
+ };
72
+ function getTemplate(agent) {
73
+ return exports.templates[agent];
74
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "mspec",
3
+ "version": "0.0.1",
4
+ "description": "A minimalist Spec-Driven Development (SDD) toolkit for solo developers and AI agents",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "build": "tsc"
9
+ },
10
+ "keywords": "mspec,spec,sdd,ai,claude,gemini,cursor,zed,opencode",
11
+ "author": "rzkmak",
12
+ "license": "ISC",
13
+ "type": "commonjs",
14
+ "dependencies": {
15
+ "@types/node": "^25.3.5",
16
+ "chalk": "^4.1.2",
17
+ "commander": "^14.0.3",
18
+ "enquirer": "^2.4.1",
19
+ "ts-node": "^10.9.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/jest": "^30.0.0",
23
+ "jest": "^30.2.0",
24
+ "ts-jest": "^29.4.6",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "bin": {
28
+ "mspec": "./dist/index.js"
29
+ },
30
+ "repository": "https://github.com/rzkmak/mspec",
31
+ "files": [
32
+ "dist"
33
+ ]
34
+ }