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 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
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node