@vibedx/vibekit 0.9.0 → 0.11.0
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 +37 -0
- package/assets/config.yml +10 -0
- package/assets/docs/code-doc.md +39 -0
- package/assets/docs/default.md +35 -0
- package/assets/docs/design-doc.md +43 -0
- package/assets/docs/faq.md +33 -0
- package/index.js +1 -1
- package/package.json +3 -2
- package/src/commands/docs/add.js +162 -0
- package/src/commands/docs/index.js +47 -0
- package/src/commands/docs/index.test.js +130 -0
- package/src/commands/docs/list.js +137 -0
- package/src/commands/docs/refine.js +138 -0
- package/src/commands/docs/show.js +28 -0
- package/src/commands/init/index.js +13 -1
- package/src/utils/doc.js +137 -0
- package/src/utils/doc.test.js +112 -0
- package/vibekit-plugin/.claude-plugin/plugin.json +20 -0
- package/vibekit-plugin/README.md +57 -0
- package/vibekit-plugin/agents/reviewer.md +47 -0
- package/vibekit-plugin/agents/ticket-worker.md +47 -0
- package/vibekit-plugin/hooks/detect-vibe.sh +15 -0
- package/vibekit-plugin/hooks/hooks.json +17 -0
- package/vibekit-plugin/settings.json +9 -0
- package/vibekit-plugin/skills/ticket-writer/SKILL.md +80 -0
- package/vibekit-plugin/skills/vibekit-workflow/SKILL.md +319 -0
package/README.md
CHANGED
|
@@ -54,6 +54,22 @@ The skill teaches agents the ticket-driven workflow — they'll create focused t
|
|
|
54
54
|
|
|
55
55
|
**Coordinating multiple agents?** See **[docs/agent-workflow.md](./docs/agent-workflow.md)** for a framework-agnostic pattern for running multi-agent teams on a shared repo — assignees, polling loops, escalation, and loop prevention.
|
|
56
56
|
|
|
57
|
+
### 🧩 Claude Code Plugin
|
|
58
|
+
|
|
59
|
+
Prefer Claude Code? Install the vibekit plugin to get the full ticket-driven workflow, agents, and hooks built in:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/plugin install vibekit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The plugin bundles:
|
|
66
|
+
|
|
67
|
+
- **Skills** — `vibekit-workflow` (full create → start → implement → close flow) and `ticket-writer` (well-structured tickets with acceptance criteria)
|
|
68
|
+
- **Agents** — `ticket-worker` (reads a ticket, implements it, closes it) and `reviewer` (checks completed work against acceptance criteria)
|
|
69
|
+
- **Hooks** — `SessionStart` detects your `.vibe/` directory and surfaces open ticket counts
|
|
70
|
+
|
|
71
|
+
See **[vibekit-plugin/](./vibekit-plugin/)** for details. The CLI is optional but recommended (`npm install -g @vibedx/vibekit`).
|
|
72
|
+
|
|
57
73
|
## 🤔 Why VibeKit?
|
|
58
74
|
|
|
59
75
|
- **🎯 Vibe code with manageable smaller tasks** - Break down complex features into focused tickets
|
|
@@ -139,6 +155,27 @@ vibe plan to-ticket .vibe/plans/my-feature.md --auto
|
|
|
139
155
|
vibe plan to-ticket .vibe/plans/my-feature.md --dry-run
|
|
140
156
|
```
|
|
141
157
|
|
|
158
|
+
### 📚 Docs
|
|
159
|
+
```bash
|
|
160
|
+
# Create a doc from a template (types: guide, design-doc, code-doc, faq)
|
|
161
|
+
vibe docs add "Getting Started"
|
|
162
|
+
vibe docs add "Docs Architecture" --type design-doc
|
|
163
|
+
|
|
164
|
+
# Also open it in $EDITOR after creating
|
|
165
|
+
vibe docs add "API Reference" --type code-doc --edit
|
|
166
|
+
|
|
167
|
+
# List docs, with optional filters
|
|
168
|
+
vibe docs list
|
|
169
|
+
vibe docs list --type design-doc --status draft --tag api
|
|
170
|
+
|
|
171
|
+
# Print a doc to stdout
|
|
172
|
+
vibe docs show DOC-001
|
|
173
|
+
|
|
174
|
+
# AI refinement pass — expands sections and fills placeholders
|
|
175
|
+
vibe docs refine DOC-001
|
|
176
|
+
```
|
|
177
|
+
Docs live in `.vibe/docs/` with their own `DOC-NNN` sequence (independent from tickets). Templates are scaffolded into `.vibe/.templates/docs/` on `vibe init`.
|
|
178
|
+
|
|
142
179
|
### 🔀 Pull Requests
|
|
143
180
|
```bash
|
|
144
181
|
# Open a GitHub PR from the current ticket branch (title/body from the ticket)
|
package/assets/config.yml
CHANGED
|
@@ -21,6 +21,16 @@ tickets:
|
|
|
21
21
|
- high
|
|
22
22
|
- critical
|
|
23
23
|
|
|
24
|
+
docs:
|
|
25
|
+
path: ".vibe/docs"
|
|
26
|
+
id_format: "DOC-{number}"
|
|
27
|
+
default_template: ".vibe/.templates/docs/default.md"
|
|
28
|
+
default_type: guide
|
|
29
|
+
status_options:
|
|
30
|
+
- draft
|
|
31
|
+
- review
|
|
32
|
+
- published
|
|
33
|
+
|
|
24
34
|
team:
|
|
25
35
|
path: .vibe/team.yml
|
|
26
36
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: {id}
|
|
3
|
+
title: {title}
|
|
4
|
+
slug: {slug}
|
|
5
|
+
type: code-doc
|
|
6
|
+
status: draft
|
|
7
|
+
tags: []
|
|
8
|
+
author: ""
|
|
9
|
+
created_at: {date}
|
|
10
|
+
updated_at: {date}
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Summary
|
|
14
|
+
|
|
15
|
+
<!-- What does this module/component/service do, in one or two sentences? -->
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
<!-- How it's structured: key files, modules, and how they fit together. -->
|
|
20
|
+
|
|
21
|
+
## Public API
|
|
22
|
+
|
|
23
|
+
<!-- Functions, classes, or endpoints exposed. Include signatures and `code` examples. -->
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
<!-- How to use it, with examples. -->
|
|
28
|
+
|
|
29
|
+
## Internals
|
|
30
|
+
|
|
31
|
+
<!-- Notable implementation details, invariants, and gotchas. -->
|
|
32
|
+
|
|
33
|
+
## Testing
|
|
34
|
+
|
|
35
|
+
<!-- How this code is tested and how to run those tests. -->
|
|
36
|
+
|
|
37
|
+
## AI Prompt
|
|
38
|
+
|
|
39
|
+
<!-- Instructions for `vibe docs refine` to document undocumented parts of the code. -->
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: {id}
|
|
3
|
+
title: {title}
|
|
4
|
+
slug: {slug}
|
|
5
|
+
type: guide
|
|
6
|
+
status: draft
|
|
7
|
+
tags: []
|
|
8
|
+
author: ""
|
|
9
|
+
created_at: {date}
|
|
10
|
+
updated_at: {date}
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
<!-- What is this guide about? Who is it for and what will they be able to do after reading it? -->
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
<!-- Anything the reader needs to know or have set up first. -->
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
<!-- Walk through the process step by step. Use code blocks for commands and file paths. -->
|
|
24
|
+
|
|
25
|
+
## Examples
|
|
26
|
+
|
|
27
|
+
<!-- Concrete examples that show the guide in action. -->
|
|
28
|
+
|
|
29
|
+
## References
|
|
30
|
+
|
|
31
|
+
<!-- Links to related docs, tickets, or external resources. -->
|
|
32
|
+
|
|
33
|
+
## AI Prompt
|
|
34
|
+
|
|
35
|
+
<!-- Instructions for `vibe docs refine` to expand or improve this guide. -->
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: {id}
|
|
3
|
+
title: {title}
|
|
4
|
+
slug: {slug}
|
|
5
|
+
type: design-doc
|
|
6
|
+
status: draft
|
|
7
|
+
tags: []
|
|
8
|
+
author: ""
|
|
9
|
+
created_at: {date}
|
|
10
|
+
updated_at: {date}
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context & Problem
|
|
14
|
+
|
|
15
|
+
<!-- What problem are we solving? Why now? What's the current state? -->
|
|
16
|
+
|
|
17
|
+
## Goals
|
|
18
|
+
|
|
19
|
+
<!-- What must this design achieve? -->
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
<!-- Explicitly out of scope. -->
|
|
24
|
+
|
|
25
|
+
## Proposed Design
|
|
26
|
+
|
|
27
|
+
<!-- The core of the doc: architecture, data flow, key decisions. -->
|
|
28
|
+
|
|
29
|
+
## Alternatives Considered
|
|
30
|
+
|
|
31
|
+
<!-- Other approaches and why they were not chosen. -->
|
|
32
|
+
|
|
33
|
+
## Risks & Trade-offs
|
|
34
|
+
|
|
35
|
+
<!-- What could go wrong, and what are we trading away? -->
|
|
36
|
+
|
|
37
|
+
## Rollout Plan
|
|
38
|
+
|
|
39
|
+
<!-- How this ships: phases, migrations, feature flags. -->
|
|
40
|
+
|
|
41
|
+
## AI Prompt
|
|
42
|
+
|
|
43
|
+
<!-- Instructions for `vibe docs refine` to pressure-test or expand this design. -->
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: {id}
|
|
3
|
+
title: {title}
|
|
4
|
+
slug: {slug}
|
|
5
|
+
type: faq
|
|
6
|
+
status: draft
|
|
7
|
+
tags: []
|
|
8
|
+
author: ""
|
|
9
|
+
created_at: {date}
|
|
10
|
+
updated_at: {date}
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
<!-- What topic does this FAQ cover? -->
|
|
16
|
+
|
|
17
|
+
## Questions
|
|
18
|
+
|
|
19
|
+
### Q: <question here>
|
|
20
|
+
|
|
21
|
+
<!-- Answer. -->
|
|
22
|
+
|
|
23
|
+
### Q: <question here>
|
|
24
|
+
|
|
25
|
+
<!-- Answer. -->
|
|
26
|
+
|
|
27
|
+
## Still stuck?
|
|
28
|
+
|
|
29
|
+
<!-- Where to go for more help: docs, channels, contacts. -->
|
|
30
|
+
|
|
31
|
+
## AI Prompt
|
|
32
|
+
|
|
33
|
+
<!-- Instructions for `vibe docs refine` to add or improve answers. -->
|
package/index.js
CHANGED
|
@@ -24,7 +24,7 @@ const __dirname = dirname(__filename);
|
|
|
24
24
|
const AVAILABLE_COMMANDS = [
|
|
25
25
|
'init', 'new', 'close', 'list', 'get-started',
|
|
26
26
|
'start', 'link', 'unlink', 'refine', 'lint', 'review', 'team', 'skills',
|
|
27
|
-
'status', 'stats', 'plan', 'pr', 'swarm'
|
|
27
|
+
'status', 'stats', 'plan', 'pr', 'swarm', 'docs'
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibedx/vibekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A powerful CLI tool for managing development tickets and project workflows",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"index.js",
|
|
9
9
|
"src/",
|
|
10
10
|
"assets/",
|
|
11
|
-
"skills/"
|
|
11
|
+
"skills/",
|
|
12
|
+
"vibekit-plugin/"
|
|
12
13
|
],
|
|
13
14
|
"bin": {
|
|
14
15
|
"vibe": "index.js"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import { getProjectRoot, getConfig, createSlug } from '../../utils/index.js';
|
|
7
|
+
import { getDocsDir, getNextDocId } from '../../utils/doc.js';
|
|
8
|
+
import { logger } from '../../utils/cli.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TYPE = 'guide';
|
|
14
|
+
|
|
15
|
+
// Doc type -> template filename (bundled and project-local share names, guide -> default)
|
|
16
|
+
const TYPE_TEMPLATES = {
|
|
17
|
+
guide: 'default.md',
|
|
18
|
+
'design-doc': 'design-doc.md',
|
|
19
|
+
'code-doc': 'code-doc.md',
|
|
20
|
+
faq: 'faq.md'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse `vibe docs add` arguments.
|
|
25
|
+
* @param {string[]} args
|
|
26
|
+
* @returns {{title: string, type: string, edit: boolean}}
|
|
27
|
+
*/
|
|
28
|
+
function parseArguments(args) {
|
|
29
|
+
const titleParts = [];
|
|
30
|
+
let type = null;
|
|
31
|
+
let edit = false;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg === '--type' && i + 1 < args.length) {
|
|
36
|
+
type = args[i + 1];
|
|
37
|
+
i++;
|
|
38
|
+
} else if (arg === '--edit' || arg === '-e') {
|
|
39
|
+
edit = true;
|
|
40
|
+
} else if (!arg.startsWith('--')) {
|
|
41
|
+
titleParts.push(arg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const title = titleParts.join(' ').trim();
|
|
46
|
+
if (!title) {
|
|
47
|
+
throw new Error('Please provide a title. Usage: vibe docs add "title" [--type <type>]');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { title, type, edit };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the type, falling back to the default if unrecognised.
|
|
55
|
+
* @param {string|null} type
|
|
56
|
+
* @param {Object} config
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function resolveType(type, config) {
|
|
60
|
+
const defaultType = config.docs?.default_type || DEFAULT_TYPE;
|
|
61
|
+
if (!type) {
|
|
62
|
+
return defaultType;
|
|
63
|
+
}
|
|
64
|
+
if (!TYPE_TEMPLATES[type]) {
|
|
65
|
+
logger.warning(`Unknown doc type '${type}'. Falling back to '${defaultType}'.`);
|
|
66
|
+
return defaultType;
|
|
67
|
+
}
|
|
68
|
+
return type;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load the template for a type: project-local first, then bundled asset.
|
|
73
|
+
* @param {string} type
|
|
74
|
+
* @param {string} root
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function loadTemplate(type, root) {
|
|
78
|
+
const templateFile = TYPE_TEMPLATES[type] || TYPE_TEMPLATES[DEFAULT_TYPE];
|
|
79
|
+
const projectTemplate = path.join(root, '.vibe', '.templates', 'docs', templateFile);
|
|
80
|
+
const bundledTemplate = path.join(__dirname, '../../../assets', 'docs', templateFile);
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(projectTemplate)) {
|
|
83
|
+
return fs.readFileSync(projectTemplate, 'utf-8');
|
|
84
|
+
}
|
|
85
|
+
if (fs.existsSync(bundledTemplate)) {
|
|
86
|
+
return fs.readFileSync(bundledTemplate, 'utf-8');
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`No template found for type '${type}'. Run "vibe init" to scaffold doc templates.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fill template placeholders.
|
|
93
|
+
* @param {string} template
|
|
94
|
+
* @param {{id: string, title: string, slug: string, type: string, timestamp: string}} data
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function fillTemplate(template, { id, title, slug, type, timestamp }) {
|
|
98
|
+
return template
|
|
99
|
+
.replace(/{id}/g, id)
|
|
100
|
+
.replace(/{title}/g, title)
|
|
101
|
+
.replace(/{slug}/g, slug)
|
|
102
|
+
.replace(/{date}/g, timestamp)
|
|
103
|
+
.replace(/^type: .*$/m, `type: ${type}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Open a file in $EDITOR.
|
|
108
|
+
* @param {string} filePath
|
|
109
|
+
*/
|
|
110
|
+
function openInEditor(filePath) {
|
|
111
|
+
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
112
|
+
if (!editor) {
|
|
113
|
+
logger.warning('No $EDITOR set — skipping auto-open.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const child = spawn(editor, [filePath], { stdio: 'inherit' });
|
|
117
|
+
child.on('error', (err) => {
|
|
118
|
+
logger.warning(`Could not open editor: ${err.message}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* `vibe docs add` — create a new doc from a template.
|
|
124
|
+
* @param {string[]} args
|
|
125
|
+
*/
|
|
126
|
+
async function addDoc(args) {
|
|
127
|
+
try {
|
|
128
|
+
const { title, type: rawType, edit } = parseArguments(args);
|
|
129
|
+
const root = getProjectRoot();
|
|
130
|
+
const config = getConfig();
|
|
131
|
+
|
|
132
|
+
const type = resolveType(rawType, config);
|
|
133
|
+
const template = loadTemplate(type, root);
|
|
134
|
+
|
|
135
|
+
const id = getNextDocId();
|
|
136
|
+
const slug = createSlug(title, config);
|
|
137
|
+
const filename = `${id}-${slug}.md`;
|
|
138
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
139
|
+
|
|
140
|
+
const docsDir = getDocsDir();
|
|
141
|
+
if (!fs.existsSync(docsDir)) {
|
|
142
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const outputPath = path.join(docsDir, filename);
|
|
146
|
+
const content = fillTemplate(template, { id, title, slug, type, timestamp });
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(outputPath, content, 'utf-8');
|
|
149
|
+
|
|
150
|
+
const relativePath = path.relative(process.cwd(), outputPath);
|
|
151
|
+
logger.success(`Created ${id} (${type}): ${relativePath}`);
|
|
152
|
+
|
|
153
|
+
if (edit) {
|
|
154
|
+
openInEditor(outputPath);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.error(error.message);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default addDoc;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { logger } from '../../utils/cli.js';
|
|
2
|
+
import addDoc from './add.js';
|
|
3
|
+
import listDocs from './list.js';
|
|
4
|
+
import refineDoc from './refine.js';
|
|
5
|
+
import showDoc from './show.js';
|
|
6
|
+
|
|
7
|
+
const SUBCOMMANDS = {
|
|
8
|
+
add: addDoc,
|
|
9
|
+
list: listDocs,
|
|
10
|
+
refine: refineDoc,
|
|
11
|
+
show: showDoc
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function showHelp() {
|
|
15
|
+
console.log(`\n📚 vibe docs — project documentation\n`);
|
|
16
|
+
console.log('Usage: vibe docs <subcommand> [options]\n');
|
|
17
|
+
console.log('Subcommands:');
|
|
18
|
+
console.log(' add "title" [--type guide|design-doc|code-doc|faq] [--edit] Create a new doc');
|
|
19
|
+
console.log(' list [--type <type>] [--status <status>] [--tag <tag>] List docs');
|
|
20
|
+
console.log(' show <id> Print a doc to stdout');
|
|
21
|
+
console.log(' refine <id> AI refinement pass\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `vibe docs` entry point — routes to subcommands.
|
|
26
|
+
* @param {string[]} args - Command arguments
|
|
27
|
+
*/
|
|
28
|
+
async function docsCommand(args = []) {
|
|
29
|
+
const [subcommand, ...rest] = args;
|
|
30
|
+
|
|
31
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
32
|
+
showHelp();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handler = SUBCOMMANDS[subcommand];
|
|
37
|
+
|
|
38
|
+
if (!handler) {
|
|
39
|
+
logger.error(`Unknown docs subcommand: ${subcommand}`);
|
|
40
|
+
showHelp();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await handler(rest);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default docsCommand;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import {
|
|
6
|
+
createTempDir,
|
|
7
|
+
cleanupTempDir,
|
|
8
|
+
mockConsole,
|
|
9
|
+
mockProcessCwd,
|
|
10
|
+
mockProcessExit,
|
|
11
|
+
createMockVibeProject
|
|
12
|
+
} from '../../utils/test-helpers.js';
|
|
13
|
+
import addDoc from './add.js';
|
|
14
|
+
import listDocs from './list.js';
|
|
15
|
+
import showDoc from './show.js';
|
|
16
|
+
|
|
17
|
+
function readFrontmatter(filePath) {
|
|
18
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
19
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20
|
+
return yaml.load(match[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('docs command', () => {
|
|
24
|
+
let tempDir;
|
|
25
|
+
let consoleMock;
|
|
26
|
+
let restoreCwd;
|
|
27
|
+
let exitMock;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tempDir = createTempDir('docs-test');
|
|
31
|
+
consoleMock = mockConsole();
|
|
32
|
+
restoreCwd = mockProcessCwd(tempDir);
|
|
33
|
+
exitMock = mockProcessExit();
|
|
34
|
+
createMockVibeProject(tempDir);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
consoleMock.restore();
|
|
39
|
+
restoreCwd();
|
|
40
|
+
exitMock.restore();
|
|
41
|
+
cleanupTempDir(tempDir);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('add', () => {
|
|
45
|
+
it('creates a DOC-001 file with correct frontmatter', async () => {
|
|
46
|
+
await addDoc(['My First Guide']);
|
|
47
|
+
|
|
48
|
+
const docsDir = path.join(tempDir, '.vibe', 'docs');
|
|
49
|
+
const files = fs.readdirSync(docsDir);
|
|
50
|
+
expect(files).toHaveLength(1);
|
|
51
|
+
expect(files[0]).toMatch(/^DOC-001-my-first-guide\.md$/);
|
|
52
|
+
|
|
53
|
+
const fm = readFrontmatter(path.join(docsDir, files[0]));
|
|
54
|
+
expect(fm.id).toBe('DOC-001');
|
|
55
|
+
expect(fm.title).toBe('My First Guide');
|
|
56
|
+
expect(fm.type).toBe('guide');
|
|
57
|
+
expect(fm.status).toBe('draft');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('assigns sequential IDs', async () => {
|
|
61
|
+
await addDoc(['First']);
|
|
62
|
+
await addDoc(['Second']);
|
|
63
|
+
|
|
64
|
+
const docsDir = path.join(tempDir, '.vibe', 'docs');
|
|
65
|
+
const ids = fs.readdirSync(docsDir).map(f => readFrontmatter(path.join(docsDir, f)).id).sort();
|
|
66
|
+
expect(ids).toEqual(['DOC-001', 'DOC-002']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('selects the template for --type', async () => {
|
|
70
|
+
await addDoc(['A Design', '--type', 'design-doc']);
|
|
71
|
+
|
|
72
|
+
const docsDir = path.join(tempDir, '.vibe', 'docs');
|
|
73
|
+
const file = fs.readdirSync(docsDir)[0];
|
|
74
|
+
const content = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
75
|
+
expect(readFrontmatter(path.join(docsDir, file)).type).toBe('design-doc');
|
|
76
|
+
expect(content).toContain('## Proposed Design');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to guide for an unknown type', async () => {
|
|
80
|
+
await addDoc(['Weird', '--type', 'bogus']);
|
|
81
|
+
|
|
82
|
+
const docsDir = path.join(tempDir, '.vibe', 'docs');
|
|
83
|
+
const file = fs.readdirSync(docsDir)[0];
|
|
84
|
+
expect(readFrontmatter(path.join(docsDir, file)).type).toBe('guide');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('exits with an error when no title is given', async () => {
|
|
88
|
+
await expect(addDoc([])).rejects.toThrow(/process.exit/);
|
|
89
|
+
expect(exitMock.exitCalls).toContain(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('list', () => {
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await addDoc(['Alpha Guide']);
|
|
96
|
+
await addDoc(['Beta Design', '--type', 'design-doc']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('lists all docs', () => {
|
|
100
|
+
listDocs([]);
|
|
101
|
+
const output = consoleMock.logs.log.join('\n');
|
|
102
|
+
expect(output).toContain('DOC-001');
|
|
103
|
+
expect(output).toContain('DOC-002');
|
|
104
|
+
expect(output).toContain('Found 2 doc(s)');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('filters by type', () => {
|
|
108
|
+
listDocs(['--type', 'design-doc']);
|
|
109
|
+
const output = consoleMock.logs.log.join('\n');
|
|
110
|
+
expect(output).toContain('Beta Design');
|
|
111
|
+
expect(output).toContain('Found 1 doc(s) (type: design-doc)');
|
|
112
|
+
expect(output).not.toContain('Alpha Guide');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('show', () => {
|
|
117
|
+
it('prints a doc by id', async () => {
|
|
118
|
+
await addDoc(['Show Me']);
|
|
119
|
+
showDoc(['1']);
|
|
120
|
+
const output = consoleMock.logs.log.join('\n');
|
|
121
|
+
expect(output).toContain('id: DOC-001');
|
|
122
|
+
expect(output).toContain('title: Show Me');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('errors on a missing doc', () => {
|
|
126
|
+
expect(() => showDoc(['999'])).toThrow(/process.exit/);
|
|
127
|
+
expect(exitMock.exitCalls).toContain(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|