spets 0.1.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/chunk-KZQ5KNMC.js +210 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1424 -0
- package/dist/state-MCFJFWJC.js +30 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 eatnug
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Spets
|
|
2
|
+
|
|
3
|
+
Spec Driven Development Execution Framework - 유저가 정의한 스텝대로 SDD를 실행하는 CLI
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g spets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 프로젝트에서 초기화
|
|
15
|
+
spets init
|
|
16
|
+
|
|
17
|
+
# 워크플로우 시작
|
|
18
|
+
spets start "TODO 앱 만들어줘"
|
|
19
|
+
|
|
20
|
+
# 상태 확인
|
|
21
|
+
spets status
|
|
22
|
+
|
|
23
|
+
# 중단된 워크플로우 재개
|
|
24
|
+
spets resume
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How it Works
|
|
28
|
+
|
|
29
|
+
1. **spets init** - `.sept/` 폴더에 설정과 스텝 템플릿 생성
|
|
30
|
+
2. **spets start** - 유저 쿼리로 워크플로우 시작, Claude가 각 스텝 문서 생성
|
|
31
|
+
3. **approve/revise/reject** - 각 스텝마다 유저가 검토하고 승인
|
|
32
|
+
4. **반복** - 모든 스텝 완료까지 진행
|
|
33
|
+
|
|
34
|
+
## Directory Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
.sept/
|
|
38
|
+
├── config.yml # 워크플로우 설정
|
|
39
|
+
├── steps/
|
|
40
|
+
│ ├── 01-plan/
|
|
41
|
+
│ │ ├── instruction.md # Claude에게 주는 지시
|
|
42
|
+
│ │ └── template.md # 출력 템플릿
|
|
43
|
+
│ └── 02-implement/
|
|
44
|
+
│ ├── instruction.md
|
|
45
|
+
│ └── template.md
|
|
46
|
+
├── outputs/ # 생성된 문서들
|
|
47
|
+
│ └── <taskId>/
|
|
48
|
+
│ ├── 01-plan.md
|
|
49
|
+
│ └── 02-implement.md
|
|
50
|
+
└── hooks/ # 훅 스크립트
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## GitHub Integration
|
|
54
|
+
|
|
55
|
+
PR/Issue 코멘트로 워크플로우 제어:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# GitHub Actions 워크플로우 포함해서 초기화
|
|
59
|
+
spets init --github
|
|
60
|
+
|
|
61
|
+
# GitHub 플랫폼으로 시작
|
|
62
|
+
spets start "task" --platform github --owner myorg --repo myrepo --issue 42
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
코멘트 명령어:
|
|
66
|
+
- `/approve` - 승인하고 다음 스텝
|
|
67
|
+
- `/revise <feedback>` - 피드백과 함께 재생성
|
|
68
|
+
- `/reject` - 워크플로우 중단
|
|
69
|
+
|
|
70
|
+
## Claude Code Plugin
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Claude Code 스킬 설치
|
|
74
|
+
spets plugin install claude
|
|
75
|
+
|
|
76
|
+
# Claude Code에서 사용
|
|
77
|
+
/spets start "task description"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
`.sept/config.yml`:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
steps:
|
|
86
|
+
- 01-plan
|
|
87
|
+
- 02-implement
|
|
88
|
+
|
|
89
|
+
hooks:
|
|
90
|
+
preStep: "./hooks/pre-step.sh"
|
|
91
|
+
postStep: "./hooks/post-step.sh"
|
|
92
|
+
onApprove: "./hooks/on-approve.sh"
|
|
93
|
+
onComplete: "./hooks/on-complete.sh"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
- Node.js >= 18
|
|
99
|
+
- Claude CLI (`claude` command) or ANTHROPIC_API_KEY
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/core/state.ts
|
|
2
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join as join2 } from "path";
|
|
4
|
+
import matter from "gray-matter";
|
|
5
|
+
|
|
6
|
+
// src/core/config.ts
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
var SEPT_DIR = ".sept";
|
|
11
|
+
var CONFIG_FILE = "config.yml";
|
|
12
|
+
var STEPS_DIR = "steps";
|
|
13
|
+
function getSeptDir(cwd = process.cwd()) {
|
|
14
|
+
return join(cwd, SEPT_DIR);
|
|
15
|
+
}
|
|
16
|
+
function getConfigPath(cwd = process.cwd()) {
|
|
17
|
+
return join(getSeptDir(cwd), CONFIG_FILE);
|
|
18
|
+
}
|
|
19
|
+
function getStepsDir(cwd = process.cwd()) {
|
|
20
|
+
return join(getSeptDir(cwd), STEPS_DIR);
|
|
21
|
+
}
|
|
22
|
+
function getOutputsDir(cwd = process.cwd()) {
|
|
23
|
+
return join(getSeptDir(cwd), "outputs");
|
|
24
|
+
}
|
|
25
|
+
function septExists(cwd = process.cwd()) {
|
|
26
|
+
return existsSync(getConfigPath(cwd));
|
|
27
|
+
}
|
|
28
|
+
function loadConfig(cwd = process.cwd()) {
|
|
29
|
+
const configPath = getConfigPath(cwd);
|
|
30
|
+
if (!existsSync(configPath)) {
|
|
31
|
+
throw new Error(`Sept config not found. Run 'sept init' first.`);
|
|
32
|
+
}
|
|
33
|
+
const content = readFileSync(configPath, "utf-8");
|
|
34
|
+
const config = parseYaml(content);
|
|
35
|
+
if (!config.steps || config.steps.length === 0) {
|
|
36
|
+
throw new Error("Config must define at least one step");
|
|
37
|
+
}
|
|
38
|
+
return config;
|
|
39
|
+
}
|
|
40
|
+
function loadStepDefinition(stepName, cwd = process.cwd()) {
|
|
41
|
+
const stepDir = join(getStepsDir(cwd), stepName);
|
|
42
|
+
const instructionPath = join(stepDir, "instruction.md");
|
|
43
|
+
const templatePath = join(stepDir, "template.md");
|
|
44
|
+
if (!existsSync(instructionPath)) {
|
|
45
|
+
throw new Error(`Step '${stepName}' instruction not found at ${instructionPath}`);
|
|
46
|
+
}
|
|
47
|
+
const instruction = readFileSync(instructionPath, "utf-8");
|
|
48
|
+
const template = existsSync(templatePath) ? readFileSync(templatePath, "utf-8") : void 0;
|
|
49
|
+
return {
|
|
50
|
+
name: stepName,
|
|
51
|
+
instruction,
|
|
52
|
+
template
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/core/state.ts
|
|
57
|
+
function generateTaskId() {
|
|
58
|
+
const timestamp = Date.now().toString(36);
|
|
59
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
60
|
+
return `${timestamp}-${random}`;
|
|
61
|
+
}
|
|
62
|
+
function getTaskDir(taskId, cwd = process.cwd()) {
|
|
63
|
+
return join2(getOutputsDir(cwd), taskId);
|
|
64
|
+
}
|
|
65
|
+
function getOutputPath(taskId, stepName, cwd = process.cwd()) {
|
|
66
|
+
return join2(getTaskDir(taskId, cwd), `${stepName}.md`);
|
|
67
|
+
}
|
|
68
|
+
function ensureTaskDir(taskId, cwd = process.cwd()) {
|
|
69
|
+
const taskDir = getTaskDir(taskId, cwd);
|
|
70
|
+
if (!existsSync2(taskDir)) {
|
|
71
|
+
mkdirSync(taskDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
return taskDir;
|
|
74
|
+
}
|
|
75
|
+
function parseDocumentFrontmatter(content) {
|
|
76
|
+
try {
|
|
77
|
+
const { data } = matter(content);
|
|
78
|
+
return data;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function createDocument(content, frontmatter) {
|
|
84
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
85
|
+
const fm = {
|
|
86
|
+
status: frontmatter.status || "draft",
|
|
87
|
+
step: frontmatter.step || "",
|
|
88
|
+
created_at: frontmatter.created_at || now,
|
|
89
|
+
updated_at: now,
|
|
90
|
+
open_questions: frontmatter.open_questions
|
|
91
|
+
};
|
|
92
|
+
return matter.stringify(content, fm);
|
|
93
|
+
}
|
|
94
|
+
function updateDocumentStatus(filePath, status) {
|
|
95
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
96
|
+
const { content: body, data } = matter(content);
|
|
97
|
+
data.status = status;
|
|
98
|
+
data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
99
|
+
writeFileSync(filePath, matter.stringify(body, data));
|
|
100
|
+
}
|
|
101
|
+
function saveDocument(taskId, stepName, content, status = "draft", openQuestions, cwd = process.cwd()) {
|
|
102
|
+
ensureTaskDir(taskId, cwd);
|
|
103
|
+
const outputPath = getOutputPath(taskId, stepName, cwd);
|
|
104
|
+
const document = createDocument(content, {
|
|
105
|
+
status,
|
|
106
|
+
step: stepName,
|
|
107
|
+
open_questions: openQuestions
|
|
108
|
+
});
|
|
109
|
+
writeFileSync(outputPath, document);
|
|
110
|
+
return outputPath;
|
|
111
|
+
}
|
|
112
|
+
function loadDocument(taskId, stepName, cwd = process.cwd()) {
|
|
113
|
+
const outputPath = getOutputPath(taskId, stepName, cwd);
|
|
114
|
+
if (!existsSync2(outputPath)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const raw = readFileSync2(outputPath, "utf-8");
|
|
118
|
+
const { content, data } = matter(raw);
|
|
119
|
+
return {
|
|
120
|
+
content,
|
|
121
|
+
frontmatter: data
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function listTasks(cwd = process.cwd()) {
|
|
125
|
+
const outputsDir = getOutputsDir(cwd);
|
|
126
|
+
if (!existsSync2(outputsDir)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return readdirSync(outputsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
130
|
+
}
|
|
131
|
+
function getWorkflowState(taskId, config, cwd = process.cwd()) {
|
|
132
|
+
const taskDir = getTaskDir(taskId, cwd);
|
|
133
|
+
if (!existsSync2(taskDir)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
137
|
+
let lastApprovedIndex = -1;
|
|
138
|
+
let currentStatus = "in_progress";
|
|
139
|
+
let userQuery = "";
|
|
140
|
+
for (let i = 0; i < config.steps.length; i++) {
|
|
141
|
+
const stepName = config.steps[i];
|
|
142
|
+
const doc = loadDocument(taskId, stepName, cwd);
|
|
143
|
+
if (doc) {
|
|
144
|
+
outputs.set(stepName, {
|
|
145
|
+
path: getOutputPath(taskId, stepName, cwd),
|
|
146
|
+
status: doc.frontmatter.status
|
|
147
|
+
});
|
|
148
|
+
if (doc.frontmatter.status === "approved") {
|
|
149
|
+
lastApprovedIndex = i;
|
|
150
|
+
} else if (doc.frontmatter.status === "rejected") {
|
|
151
|
+
currentStatus = "rejected";
|
|
152
|
+
}
|
|
153
|
+
if (i === 0 && !userQuery) {
|
|
154
|
+
const queryMatch = doc.content.match(/## User Query\n\n(.+?)(?=\n\n|$)/s);
|
|
155
|
+
if (queryMatch) {
|
|
156
|
+
userQuery = queryMatch[1].trim();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (lastApprovedIndex === config.steps.length - 1) {
|
|
162
|
+
currentStatus = "completed";
|
|
163
|
+
}
|
|
164
|
+
const currentStepIndex = Math.min(lastApprovedIndex + 1, config.steps.length - 1);
|
|
165
|
+
const currentStepName = config.steps[currentStepIndex];
|
|
166
|
+
const currentDoc = loadDocument(taskId, currentStepName, cwd);
|
|
167
|
+
if (currentDoc?.frontmatter.status === "draft" && currentDoc.frontmatter.open_questions?.length) {
|
|
168
|
+
currentStatus = "paused";
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
taskId,
|
|
172
|
+
userQuery,
|
|
173
|
+
currentStepIndex,
|
|
174
|
+
currentStepName,
|
|
175
|
+
status: currentStatus,
|
|
176
|
+
outputs
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function saveTaskMetadata(taskId, userQuery, cwd = process.cwd()) {
|
|
180
|
+
const taskDir = ensureTaskDir(taskId, cwd);
|
|
181
|
+
const metaPath = join2(taskDir, ".meta.json");
|
|
182
|
+
writeFileSync(metaPath, JSON.stringify({ userQuery, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
183
|
+
}
|
|
184
|
+
function loadTaskMetadata(taskId, cwd = process.cwd()) {
|
|
185
|
+
const metaPath = join2(getTaskDir(taskId, cwd), ".meta.json");
|
|
186
|
+
if (!existsSync2(metaPath)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return JSON.parse(readFileSync2(metaPath, "utf-8"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
getSeptDir,
|
|
194
|
+
septExists,
|
|
195
|
+
loadConfig,
|
|
196
|
+
loadStepDefinition,
|
|
197
|
+
generateTaskId,
|
|
198
|
+
getTaskDir,
|
|
199
|
+
getOutputPath,
|
|
200
|
+
ensureTaskDir,
|
|
201
|
+
parseDocumentFrontmatter,
|
|
202
|
+
createDocument,
|
|
203
|
+
updateDocumentStatus,
|
|
204
|
+
saveDocument,
|
|
205
|
+
loadDocument,
|
|
206
|
+
listTasks,
|
|
207
|
+
getWorkflowState,
|
|
208
|
+
saveTaskMetadata,
|
|
209
|
+
loadTaskMetadata
|
|
210
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|