pm-skill 1.0.1 → 1.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/AGENTS.md +20 -22
- package/README.md +48 -61
- package/SKILL.md +51 -83
- package/dist/env.d.ts +8 -13
- package/dist/env.js +15 -37
- package/dist/workflows.js +99 -102
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -9,11 +9,7 @@ A structured project management CLI that integrates Linear (issue tracking) and
|
|
|
9
9
|
## How to Run Commands
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
13
|
-
pm-skill <command> [args] [flags]
|
|
14
|
-
|
|
15
|
-
# If running from source:
|
|
16
|
-
npx tsx src/workflows.ts <command> [args] [flags]
|
|
12
|
+
npx pm-skill <command> [args] [flags]
|
|
17
13
|
```
|
|
18
14
|
|
|
19
15
|
## Available Commands
|
|
@@ -21,67 +17,69 @@ npx tsx src/workflows.ts <command> [args] [flags]
|
|
|
21
17
|
### setup
|
|
22
18
|
Verify Linear/Notion connection and show team/label configuration.
|
|
23
19
|
```bash
|
|
24
|
-
pm-skill setup
|
|
20
|
+
npx pm-skill setup
|
|
21
|
+
npx pm-skill setup --sync # create missing labels in Linear
|
|
25
22
|
```
|
|
26
23
|
|
|
27
24
|
### start-feature
|
|
28
25
|
Create a Linear issue + Notion PRD page with bidirectional links.
|
|
29
26
|
```bash
|
|
30
|
-
pm-skill start-feature "<title>"
|
|
27
|
+
npx pm-skill start-feature "<title>"
|
|
31
28
|
```
|
|
32
29
|
|
|
33
30
|
### report-bug
|
|
34
31
|
File a bug report with severity-based priority mapping.
|
|
35
32
|
```bash
|
|
36
|
-
pm-skill report-bug "<title>" --severity <urgent|high|medium|low>
|
|
33
|
+
npx pm-skill report-bug "<title>" --severity <urgent|high|medium|low>
|
|
37
34
|
```
|
|
38
35
|
Default severity: medium.
|
|
39
36
|
|
|
40
37
|
### add-task
|
|
41
38
|
Add a sub-issue to a parent issue.
|
|
42
39
|
```bash
|
|
43
|
-
pm-skill add-task <parent-issue-id> "<title>"
|
|
40
|
+
npx pm-skill add-task <parent-issue-id> "<title>"
|
|
44
41
|
```
|
|
45
42
|
|
|
46
43
|
### relate
|
|
47
44
|
Set a relationship between two issues.
|
|
48
45
|
```bash
|
|
49
|
-
pm-skill relate <issue1> <issue2> --type <related|similar>
|
|
46
|
+
npx pm-skill relate <issue1> <issue2> --type <related|similar>
|
|
50
47
|
```
|
|
51
48
|
Default type: related.
|
|
52
49
|
|
|
53
50
|
### block
|
|
54
51
|
Set a blocking dependency (issue1 blocks issue2).
|
|
55
52
|
```bash
|
|
56
|
-
pm-skill block <blocker-issue> <blocked-issue>
|
|
53
|
+
npx pm-skill block <blocker-issue> <blocked-issue>
|
|
57
54
|
```
|
|
58
55
|
|
|
59
56
|
### attach-doc
|
|
60
57
|
Attach a document URL to an issue with type validation.
|
|
61
58
|
```bash
|
|
62
|
-
pm-skill attach-doc <issue> --url "<url>" --title "<title>" --type <source-of-truth|issue-tracking|domain-knowledge>
|
|
59
|
+
npx pm-skill attach-doc <issue> --url "<url>" --title "<title>" --type <source-of-truth|issue-tracking|domain-knowledge>
|
|
63
60
|
```
|
|
64
61
|
|
|
65
62
|
### get
|
|
66
63
|
Show issue details including children, relations, and attachments.
|
|
67
64
|
```bash
|
|
68
|
-
pm-skill get <issue-id>
|
|
65
|
+
npx pm-skill get <issue-id>
|
|
69
66
|
```
|
|
70
67
|
|
|
71
68
|
## Configuration
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
-
|
|
70
|
+
All config is per-project (CWD):
|
|
71
|
+
- **`.env`** — API keys and IDs
|
|
72
|
+
- **`config.yml`** — Labels, templates, priorities, severity mappings
|
|
75
73
|
|
|
76
74
|
## Workflow Patterns
|
|
77
75
|
|
|
78
76
|
### Feature Development
|
|
79
|
-
1. `start-feature "Feature Name"` — creates Linear issue + Notion PRD
|
|
80
|
-
2. `add-task ENG-XX "Sub-task 1"` — break down into sub-tasks
|
|
81
|
-
3. `relate ENG-XX ENG-YY` — link related issues
|
|
82
|
-
4. `attach-doc ENG-XX --url "..." --title "..." --type source-of-truth`
|
|
77
|
+
1. `npx pm-skill start-feature "Feature Name"` — creates Linear issue + Notion PRD
|
|
78
|
+
2. `npx pm-skill add-task ENG-XX "Sub-task 1"` — break down into sub-tasks
|
|
79
|
+
3. `npx pm-skill relate ENG-XX ENG-YY` — link related issues
|
|
80
|
+
4. `npx pm-skill attach-doc ENG-XX --url "..." --title "..." --type source-of-truth`
|
|
83
81
|
|
|
84
82
|
### Bug Fix
|
|
85
|
-
1. `report-bug "Bug Description" --severity high`
|
|
86
|
-
2. `add-task ENG-XX "Root cause analysis"`
|
|
87
|
-
3. `add-task ENG-XX "Fix and test"`
|
|
83
|
+
1. `npx pm-skill report-bug "Bug Description" --severity high`
|
|
84
|
+
2. `npx pm-skill add-task ENG-XX "Root cause analysis"`
|
|
85
|
+
3. `npx pm-skill add-task ENG-XX "Fix and test"`
|
package/README.md
CHANGED
|
@@ -6,77 +6,61 @@ Structured project management CLI that integrates **Linear** and **Notion**. Des
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
+
- **init** — Validate API keys, create `.env`, `config.yml`, `SKILL.md`, `AGENTS.md` in your project
|
|
10
|
+
- **setup** — Verify connections, label matching, Notion status (`--sync` creates missing labels)
|
|
9
11
|
- **start-feature** — Create Linear issue + Notion PRD page, auto-linked
|
|
10
12
|
- **report-bug** — File bug with severity-based priority mapping
|
|
11
13
|
- **add-task** — Add sub-issues to a parent
|
|
12
14
|
- **relate / block** — Set issue relationships and dependencies
|
|
13
15
|
- **attach-doc** — Attach documents with type validation
|
|
14
16
|
- **get** — View issue details with children, relations, and attachments
|
|
15
|
-
- **setup** — Verify connections and discover team/label IDs
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
### Option A: npm (recommended)
|
|
18
|
+
## Quick Start
|
|
20
19
|
|
|
21
20
|
```bash
|
|
22
|
-
|
|
23
|
-
pm-skill help
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Option B: npx (no install)
|
|
21
|
+
cd your-project
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
npx pm-skill
|
|
30
|
-
```
|
|
23
|
+
# Initialize (validates keys, creates config files)
|
|
24
|
+
npx pm-skill init --linear-key lin_api_xxx --notion-key secret_xxx
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
# Verify label matching
|
|
27
|
+
npx pm-skill setup
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cd ~/.claude/skills/pm-skill && npm install
|
|
29
|
+
# Create missing labels in Linear
|
|
30
|
+
npx pm-skill setup --sync
|
|
37
31
|
```
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
This creates the following in your project:
|
|
40
34
|
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
35
|
+
```
|
|
36
|
+
your-project/
|
|
37
|
+
├── .env # API keys + project settings
|
|
38
|
+
├── config.yml # Labels, templates, priorities
|
|
39
|
+
├── .claude/skills/pm-skill/SKILL.md # Claude Code auto-discovers this
|
|
40
|
+
├── AGENTS.md # Codex auto-discovers this
|
|
41
|
+
└── ...
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
## Setup
|
|
48
|
-
|
|
49
|
-
### 1. Create `.env`
|
|
50
|
-
|
|
51
|
-
Place `.env` in any of these locations (checked in order):
|
|
52
|
-
1. Current working directory
|
|
53
|
-
2. `~/.pm-skill/`
|
|
54
|
-
3. Package root
|
|
44
|
+
## Setup Details
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
# Copy the template
|
|
58
|
-
cp .env.example ~/.pm-skill/.env
|
|
59
|
-
# Edit with your API keys
|
|
60
|
-
```
|
|
46
|
+
### API Keys
|
|
61
47
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
LINEAR_DEFAULT_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
65
|
-
NOTION_API_KEY=secret_xxxxxxxx
|
|
66
|
-
NOTION_ROOT_PAGE_ID=xxxxxxxx
|
|
67
|
-
```
|
|
48
|
+
- **Linear**: Settings > API > Personal API Keys
|
|
49
|
+
- **Notion**: https://www.notion.so/my-integrations > New integration
|
|
68
50
|
|
|
69
|
-
###
|
|
51
|
+
### init options
|
|
70
52
|
|
|
71
53
|
```bash
|
|
72
|
-
pm-skill
|
|
54
|
+
npx pm-skill init --linear-key <key> [options]
|
|
55
|
+
--notion-key Notion API key (optional)
|
|
56
|
+
--team-id Linear team ID (auto-detected if omitted)
|
|
57
|
+
--project-id Linear project ID (optional)
|
|
58
|
+
--notion-page Notion root page ID (optional)
|
|
73
59
|
```
|
|
74
60
|
|
|
75
|
-
|
|
61
|
+
### Customize `config.yml`
|
|
76
62
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Copy `config.yml` to `~/.pm-skill/config.yml` and edit labels, templates, priorities, and severity mappings to match your project.
|
|
63
|
+
Edit `config.yml` in your project root to match your labels, templates, priorities, and severity mappings.
|
|
80
64
|
|
|
81
65
|
**Rule: every label and template must have a `description` field.**
|
|
82
66
|
|
|
@@ -84,48 +68,47 @@ Copy `config.yml` to `~/.pm-skill/config.yml` and edit labels, templates, priori
|
|
|
84
68
|
|
|
85
69
|
```bash
|
|
86
70
|
# Start a feature
|
|
87
|
-
pm-skill start-feature "Booking cancellation"
|
|
71
|
+
npx pm-skill start-feature "Booking cancellation"
|
|
88
72
|
|
|
89
73
|
# Report a bug
|
|
90
|
-
pm-skill report-bug "Payment amount error" --severity high
|
|
74
|
+
npx pm-skill report-bug "Payment amount error" --severity high
|
|
91
75
|
|
|
92
76
|
# Add sub-tasks
|
|
93
|
-
pm-skill add-task ENG-10 "Write unit tests"
|
|
94
|
-
pm-skill add-task ENG-10 "Frontend UI"
|
|
77
|
+
npx pm-skill add-task ENG-10 "Write unit tests"
|
|
78
|
+
npx pm-skill add-task ENG-10 "Frontend UI"
|
|
95
79
|
|
|
96
80
|
# Link issues
|
|
97
|
-
pm-skill relate ENG-10 ENG-8 --type related
|
|
98
|
-
pm-skill block ENG-10 ENG-15
|
|
81
|
+
npx pm-skill relate ENG-10 ENG-8 --type related
|
|
82
|
+
npx pm-skill block ENG-10 ENG-15
|
|
99
83
|
|
|
100
84
|
# Attach documents
|
|
101
|
-
pm-skill attach-doc ENG-10 --url "https://notion.so/..." --title "Design Doc" --type source-of-truth
|
|
85
|
+
npx pm-skill attach-doc ENG-10 --url "https://notion.so/..." --title "Design Doc" --type source-of-truth
|
|
102
86
|
|
|
103
87
|
# View issue details
|
|
104
|
-
pm-skill get ENG-10
|
|
88
|
+
npx pm-skill get ENG-10
|
|
89
|
+
|
|
90
|
+
# Check version
|
|
91
|
+
npx pm-skill --version
|
|
105
92
|
```
|
|
106
93
|
|
|
107
94
|
## Using with AI Assistants
|
|
108
95
|
|
|
109
96
|
### Claude Code
|
|
110
97
|
|
|
111
|
-
|
|
98
|
+
After `npx pm-skill init`, Claude Code auto-discovers the skill via `.claude/skills/pm-skill/SKILL.md`. You can invoke commands through natural language:
|
|
112
99
|
|
|
113
100
|
> "Create a feature issue for booking cancellation"
|
|
114
101
|
|
|
115
102
|
### Codex
|
|
116
103
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
npx tsx src/workflows.ts start-feature "Booking cancellation"
|
|
121
|
-
```
|
|
104
|
+
After `npx pm-skill init`, Codex reads `AGENTS.md` at the project root for command instructions.
|
|
122
105
|
|
|
123
106
|
### Any AI Assistant
|
|
124
107
|
|
|
125
108
|
Any assistant that can execute shell commands can use pm-skill:
|
|
126
109
|
|
|
127
110
|
```bash
|
|
128
|
-
pm-skill start-feature "My Feature"
|
|
111
|
+
npx pm-skill start-feature "My Feature"
|
|
129
112
|
```
|
|
130
113
|
|
|
131
114
|
## Config Structure
|
|
@@ -139,6 +122,10 @@ pm-skill start-feature "My Feature"
|
|
|
139
122
|
| `doc_types` | Document types for attach-doc validation |
|
|
140
123
|
| `epics` | Epic definitions (project-specific) |
|
|
141
124
|
|
|
125
|
+
## Per-Project Model
|
|
126
|
+
|
|
127
|
+
All config is per-project. Each project gets its own `.env`, `config.yml`, and instruction files. Run `npx pm-skill init` in each project directory.
|
|
128
|
+
|
|
142
129
|
## License
|
|
143
130
|
|
|
144
131
|
MIT
|
package/SKILL.md
CHANGED
|
@@ -1,134 +1,102 @@
|
|
|
1
|
-
# PM Skill —
|
|
1
|
+
# PM Skill — Structured Project Management
|
|
2
2
|
|
|
3
|
-
Linear
|
|
4
|
-
"
|
|
3
|
+
Linear + Notion integration for structured project management.
|
|
4
|
+
"Design freedom, usage discipline" — only labels/templates/severity defined in `config.yml` are allowed.
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
### 1. API 키 발급
|
|
9
|
-
|
|
10
|
-
**Linear**: Settings > API > Personal API Keys
|
|
11
|
-
**Notion**: https://www.notion.so/my-integrations > New integration
|
|
12
|
-
|
|
13
|
-
### 2. .env 생성
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
cp .env.example .env
|
|
17
|
-
# LINEAR_API_KEY, NOTION_API_KEY 입력
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### 3. Setup 실행
|
|
6
|
+
## Setup
|
|
21
7
|
|
|
22
8
|
```bash
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
팀/상태/라벨 ID를 확인하고 `.env`에 `LINEAR_DEFAULT_TEAM_ID` 등을 설정.
|
|
9
|
+
# Initialize in project directory (validates keys, creates .env + config.yml)
|
|
10
|
+
npx pm-skill init --linear-key <key> --notion-key <key>
|
|
27
11
|
|
|
28
|
-
|
|
12
|
+
# Verify label matching
|
|
13
|
+
npx pm-skill setup
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
```yaml
|
|
34
|
-
labels:
|
|
35
|
-
- id: my-label
|
|
36
|
-
name: My Label
|
|
37
|
-
description: "이 라벨의 용도 설명" # ← 필수!
|
|
15
|
+
# Create missing labels in Linear
|
|
16
|
+
npx pm-skill setup --sync
|
|
38
17
|
```
|
|
39
18
|
|
|
40
|
-
##
|
|
19
|
+
## Commands
|
|
41
20
|
|
|
42
|
-
### setup
|
|
43
|
-
Linear/Notion
|
|
21
|
+
### setup [--sync]
|
|
22
|
+
Verify Linear/Notion connection + label matching. `--sync` creates missing labels.
|
|
44
23
|
```bash
|
|
45
|
-
npx
|
|
24
|
+
npx pm-skill setup
|
|
46
25
|
```
|
|
47
26
|
|
|
48
27
|
### start-feature
|
|
49
|
-
|
|
28
|
+
Start feature development. Creates Linear issue + Notion PRD + bidirectional links.
|
|
50
29
|
```bash
|
|
51
|
-
npx
|
|
30
|
+
npx pm-skill start-feature "Feature title"
|
|
52
31
|
```
|
|
53
32
|
|
|
54
33
|
### report-bug
|
|
55
|
-
|
|
34
|
+
File bug report. Severity maps to Linear priority.
|
|
56
35
|
```bash
|
|
57
|
-
npx
|
|
58
|
-
# severity: urgent, high, medium(
|
|
36
|
+
npx pm-skill report-bug "Bug title" --severity high
|
|
37
|
+
# severity: urgent, high, medium (default), low
|
|
59
38
|
```
|
|
60
39
|
|
|
61
40
|
### add-task
|
|
62
|
-
|
|
41
|
+
Add sub-task to an issue.
|
|
63
42
|
```bash
|
|
64
|
-
npx
|
|
43
|
+
npx pm-skill add-task ENG-10 "Write unit tests"
|
|
65
44
|
```
|
|
66
45
|
|
|
67
46
|
### relate
|
|
68
|
-
|
|
47
|
+
Link two issues. (related, similar)
|
|
69
48
|
```bash
|
|
70
|
-
npx
|
|
49
|
+
npx pm-skill relate ENG-10 ENG-11 --type related
|
|
71
50
|
```
|
|
72
51
|
|
|
73
52
|
### block
|
|
74
|
-
|
|
53
|
+
Set blocking relationship. (ENG-10 must complete before ENG-11)
|
|
75
54
|
```bash
|
|
76
|
-
npx
|
|
55
|
+
npx pm-skill block ENG-10 ENG-11
|
|
77
56
|
```
|
|
78
57
|
|
|
79
58
|
### attach-doc
|
|
80
|
-
|
|
59
|
+
Attach document URL to issue. Type is validated against config.
|
|
81
60
|
```bash
|
|
82
|
-
npx
|
|
61
|
+
npx pm-skill attach-doc ENG-10 \
|
|
83
62
|
--url "https://notion.so/..." \
|
|
84
|
-
--title "
|
|
63
|
+
--title "Design Doc" \
|
|
85
64
|
--type source-of-truth
|
|
86
65
|
# type: source-of-truth, issue-tracking, domain-knowledge
|
|
87
66
|
```
|
|
88
67
|
|
|
89
68
|
### get
|
|
90
|
-
|
|
69
|
+
Show issue details including sub-issues, relations, and attachments.
|
|
91
70
|
```bash
|
|
92
|
-
npx
|
|
71
|
+
npx pm-skill get ENG-10
|
|
93
72
|
```
|
|
94
73
|
|
|
95
|
-
##
|
|
74
|
+
## Workflow Examples
|
|
96
75
|
|
|
97
|
-
###
|
|
76
|
+
### Feature Development
|
|
98
77
|
```bash
|
|
99
|
-
|
|
100
|
-
npx
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
npx
|
|
105
|
-
npx tsx src/workflows.ts add-task ENG-10 "프론트엔드 UI"
|
|
106
|
-
npx tsx src/workflows.ts add-task ENG-10 "테스트 작성"
|
|
107
|
-
|
|
108
|
-
# 3. 관련 이슈 연결
|
|
109
|
-
npx tsx src/workflows.ts relate ENG-10 ENG-8 --type related
|
|
110
|
-
|
|
111
|
-
# 4. 선행 관계 설정
|
|
112
|
-
npx tsx src/workflows.ts block ENG-10 ENG-15
|
|
78
|
+
npx pm-skill start-feature "Booking cancellation"
|
|
79
|
+
npx pm-skill add-task ENG-10 "API endpoint"
|
|
80
|
+
npx pm-skill add-task ENG-10 "Frontend UI"
|
|
81
|
+
npx pm-skill add-task ENG-10 "Tests"
|
|
82
|
+
npx pm-skill relate ENG-10 ENG-8 --type related
|
|
83
|
+
npx pm-skill block ENG-10 ENG-15
|
|
113
84
|
```
|
|
114
85
|
|
|
115
|
-
###
|
|
86
|
+
### Bug Fix
|
|
116
87
|
```bash
|
|
117
|
-
|
|
118
|
-
npx
|
|
119
|
-
|
|
120
|
-
# 2. 하위 태스크
|
|
121
|
-
npx tsx src/workflows.ts add-task ENG-20 "원인 분석"
|
|
122
|
-
npx tsx src/workflows.ts add-task ENG-20 "수정 및 테스트"
|
|
88
|
+
npx pm-skill report-bug "Payment error" --severity high
|
|
89
|
+
npx pm-skill add-task ENG-20 "Root cause analysis"
|
|
90
|
+
npx pm-skill add-task ENG-20 "Fix and test"
|
|
123
91
|
```
|
|
124
92
|
|
|
125
|
-
## Config
|
|
93
|
+
## Config
|
|
126
94
|
|
|
127
|
-
|
|
|
128
|
-
|
|
129
|
-
| `labels` |
|
|
130
|
-
| `templates` |
|
|
131
|
-
| `priorities` |
|
|
132
|
-
| `severity_mapping` | severity
|
|
133
|
-
| `doc_types` |
|
|
134
|
-
| `epics` |
|
|
95
|
+
| Section | Description |
|
|
96
|
+
|---------|-------------|
|
|
97
|
+
| `labels` | Available labels (description required) |
|
|
98
|
+
| `templates` | Command → label/priority/Notion template mappings |
|
|
99
|
+
| `priorities` | p0-p3 → Linear priority mapping |
|
|
100
|
+
| `severity_mapping` | severity name → priority key |
|
|
101
|
+
| `doc_types` | Document types for attach-doc |
|
|
102
|
+
| `epics` | Epic definitions |
|
package/dist/env.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const PKG_ROOT: string;
|
|
2
2
|
/**
|
|
3
|
-
* Resolve a file by checking
|
|
4
|
-
* 1. CWD (project
|
|
5
|
-
* 2.
|
|
6
|
-
* 3. Package root (bundled defaults)
|
|
3
|
+
* Resolve a file by checking:
|
|
4
|
+
* 1. CWD (project config)
|
|
5
|
+
* 2. Package root (bundled defaults — for config.yml, SKILL.md, AGENTS.md)
|
|
7
6
|
*/
|
|
8
7
|
export declare function resolveFile(filename: string): string | null;
|
|
9
8
|
export interface ValidatedEnv {
|
|
@@ -15,17 +14,13 @@ export interface ValidatedEnv {
|
|
|
15
14
|
NOTION_BUG_DB_ID?: string;
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 2. ~/.pm-skill/.env (global defaults, fills in missing keys)
|
|
21
|
-
*
|
|
22
|
-
* Both files are loaded. CWD values take precedence over global.
|
|
17
|
+
* Parse CWD/.env and set values into process.env.
|
|
18
|
+
* Does NOT overwrite existing env vars.
|
|
23
19
|
*/
|
|
24
|
-
export declare function
|
|
20
|
+
export declare function loadEnvFile(): void;
|
|
25
21
|
export declare function validateEnv(command: string): ValidatedEnv;
|
|
26
22
|
/**
|
|
27
|
-
* Write key=value pairs to a .env file.
|
|
23
|
+
* Write key=value pairs to a .env file in the given directory.
|
|
28
24
|
* If the file exists, updates existing keys and appends new ones.
|
|
29
|
-
* If not, creates it fresh.
|
|
30
25
|
*/
|
|
31
26
|
export declare function writeEnvFile(targetDir: string, entries: Record<string, string>): string;
|
package/dist/env.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { resolve, dirname } from "path";
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
3
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
4
|
-
import { homedir } from "os";
|
|
5
4
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const PKG_ROOT = resolve(__dirname, "..");
|
|
7
|
-
export const GLOBAL_DIR = resolve(homedir(), ".pm-skill");
|
|
5
|
+
export const PKG_ROOT = resolve(__dirname, "..");
|
|
8
6
|
/**
|
|
9
|
-
* Resolve a file by checking
|
|
10
|
-
* 1. CWD (project
|
|
11
|
-
* 2.
|
|
12
|
-
* 3. Package root (bundled defaults)
|
|
7
|
+
* Resolve a file by checking:
|
|
8
|
+
* 1. CWD (project config)
|
|
9
|
+
* 2. Package root (bundled defaults — for config.yml, SKILL.md, AGENTS.md)
|
|
13
10
|
*/
|
|
14
11
|
export function resolveFile(filename) {
|
|
15
12
|
const candidates = [
|
|
16
13
|
resolve(process.cwd(), filename),
|
|
17
|
-
resolve(GLOBAL_DIR, filename),
|
|
18
14
|
resolve(PKG_ROOT, filename),
|
|
19
15
|
];
|
|
20
16
|
for (const p of candidates) {
|
|
@@ -27,18 +23,21 @@ const REQUIRED_KEYS = ["LINEAR_API_KEY", "LINEAR_DEFAULT_TEAM_ID"];
|
|
|
27
23
|
const NOTION_KEYS = ["NOTION_API_KEY", "NOTION_ROOT_PAGE_ID"];
|
|
28
24
|
const KEY_HELP = {
|
|
29
25
|
LINEAR_API_KEY: "Linear > Settings > API > Personal API Keys",
|
|
30
|
-
LINEAR_DEFAULT_TEAM_ID: "'pm-skill init'
|
|
26
|
+
LINEAR_DEFAULT_TEAM_ID: "'npx pm-skill init' to discover your team ID",
|
|
31
27
|
LINEAR_DEFAULT_PROJECT_ID: "Linear > Project Settings (optional)",
|
|
32
28
|
NOTION_API_KEY: "https://www.notion.so/my-integrations",
|
|
33
29
|
NOTION_ROOT_PAGE_ID: "Parent page ID where Notion docs will be created",
|
|
34
30
|
NOTION_BUG_DB_ID: "Notion DB ID for bug tracking (optional — falls back to page creation)",
|
|
35
31
|
};
|
|
36
32
|
/**
|
|
37
|
-
* Parse
|
|
38
|
-
* Does NOT overwrite existing
|
|
33
|
+
* Parse CWD/.env and set values into process.env.
|
|
34
|
+
* Does NOT overwrite existing env vars.
|
|
39
35
|
*/
|
|
40
|
-
function
|
|
41
|
-
const
|
|
36
|
+
export function loadEnvFile() {
|
|
37
|
+
const envPath = resolve(process.cwd(), ".env");
|
|
38
|
+
if (!existsSync(envPath))
|
|
39
|
+
return;
|
|
40
|
+
const content = readFileSync(envPath, "utf-8");
|
|
42
41
|
for (const line of content.split("\n")) {
|
|
43
42
|
const trimmed = line.trim();
|
|
44
43
|
if (!trimmed || trimmed.startsWith("#"))
|
|
@@ -53,25 +52,8 @@ function parseEnvFile(filePath) {
|
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Hierarchical .env loading:
|
|
58
|
-
* 1. CWD/.env (project-specific, highest priority)
|
|
59
|
-
* 2. ~/.pm-skill/.env (global defaults, fills in missing keys)
|
|
60
|
-
*
|
|
61
|
-
* Both files are loaded. CWD values take precedence over global.
|
|
62
|
-
*/
|
|
63
|
-
export function loadEnvFiles() {
|
|
64
|
-
const cwdEnv = resolve(process.cwd(), ".env");
|
|
65
|
-
const globalEnv = resolve(GLOBAL_DIR, ".env");
|
|
66
|
-
// CWD first — these values win
|
|
67
|
-
if (existsSync(cwdEnv))
|
|
68
|
-
parseEnvFile(cwdEnv);
|
|
69
|
-
// Global second — fills in anything CWD didn't set
|
|
70
|
-
if (existsSync(globalEnv))
|
|
71
|
-
parseEnvFile(globalEnv);
|
|
72
|
-
}
|
|
73
55
|
export function validateEnv(command) {
|
|
74
|
-
|
|
56
|
+
loadEnvFile();
|
|
75
57
|
const missing = [];
|
|
76
58
|
if (command === "setup" || command === "init") {
|
|
77
59
|
if (!process.env.LINEAR_API_KEY) {
|
|
@@ -96,8 +78,7 @@ export function validateEnv(command) {
|
|
|
96
78
|
.map((k) => ` ${k}: ${KEY_HELP[k] ?? ""}`)
|
|
97
79
|
.join("\n");
|
|
98
80
|
throw new Error(`Required environment variables are not set:\n${hints}\n\n` +
|
|
99
|
-
`Run 'pm-skill init'
|
|
100
|
-
`Config lookup: CWD/.env → ~/.pm-skill/.env (both loaded, CWD wins).`);
|
|
81
|
+
`Run 'npx pm-skill init' in your project directory to set up.`);
|
|
101
82
|
}
|
|
102
83
|
return {
|
|
103
84
|
LINEAR_API_KEY: process.env.LINEAR_API_KEY,
|
|
@@ -109,9 +90,8 @@ export function validateEnv(command) {
|
|
|
109
90
|
};
|
|
110
91
|
}
|
|
111
92
|
/**
|
|
112
|
-
* Write key=value pairs to a .env file.
|
|
93
|
+
* Write key=value pairs to a .env file in the given directory.
|
|
113
94
|
* If the file exists, updates existing keys and appends new ones.
|
|
114
|
-
* If not, creates it fresh.
|
|
115
95
|
*/
|
|
116
96
|
export function writeEnvFile(targetDir, entries) {
|
|
117
97
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -133,7 +113,6 @@ export function writeEnvFile(targetDir, entries) {
|
|
|
133
113
|
}
|
|
134
114
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
135
115
|
existing.set(key, trimmed);
|
|
136
|
-
// Will be replaced or kept
|
|
137
116
|
if (key in entries) {
|
|
138
117
|
lines.push(`${key}=${entries[key]}`);
|
|
139
118
|
}
|
|
@@ -142,7 +121,6 @@ export function writeEnvFile(targetDir, entries) {
|
|
|
142
121
|
}
|
|
143
122
|
}
|
|
144
123
|
}
|
|
145
|
-
// Append new keys not already in file
|
|
146
124
|
for (const [key, val] of Object.entries(entries)) {
|
|
147
125
|
if (!existing.has(key)) {
|
|
148
126
|
lines.push(`${key}=${val}`);
|
package/dist/workflows.js
CHANGED
|
@@ -1,49 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import minimist from "minimist";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, copyFileSync, mkdirSync, readFileSync } from "fs";
|
|
4
|
+
import { resolve, dirname } from "path";
|
|
5
|
+
import { validateEnv, writeEnvFile, PKG_ROOT } from "./env.js";
|
|
4
6
|
import { loadConfig, getTemplate, resolvePriority, resolveSeverity, validateDocType, validateLabel, } from "./config.js";
|
|
5
7
|
import { getLinearClient, validateLinearKey, createIssue, getIssue, getIssueDetail, createRelation, createAttachment, createLabel, getTeams, getTeamStates, getTeamLabels, resolveLabels, } from "./linear.js";
|
|
6
8
|
import { getNotionClient, createTemplatedPage, createDatabaseEntry, validateNotionKey, } from "./notion.js";
|
|
7
|
-
// ── Init
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const PKG_ROOT = resolve(__dirname, "..");
|
|
13
|
-
function copyDefaultConfig(targetDir) {
|
|
14
|
-
const targetConfig = resolve(targetDir, "config.yml");
|
|
15
|
-
if (existsSync(targetConfig)) {
|
|
16
|
-
console.log(` config.yml already exists at ${targetConfig} — skipped`);
|
|
9
|
+
// ── Init ──
|
|
10
|
+
function copyBundledFile(srcName, destPath) {
|
|
11
|
+
if (existsSync(destPath)) {
|
|
12
|
+
console.log(` ${srcName} already exists — skipped`);
|
|
17
13
|
return;
|
|
18
14
|
}
|
|
19
|
-
const src = resolve(PKG_ROOT,
|
|
15
|
+
const src = resolve(PKG_ROOT, srcName);
|
|
20
16
|
if (!existsSync(src)) {
|
|
21
|
-
console.log(
|
|
17
|
+
console.log(` Warning: bundled ${srcName} not found — skipping`);
|
|
22
18
|
return;
|
|
23
19
|
}
|
|
24
|
-
mkdirSync(
|
|
25
|
-
copyFileSync(src,
|
|
26
|
-
console.log(`
|
|
20
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
21
|
+
copyFileSync(src, destPath);
|
|
22
|
+
console.log(` ${srcName} → ${destPath}`);
|
|
27
23
|
}
|
|
28
24
|
async function init(args) {
|
|
29
25
|
const linearKey = args["linear-key"];
|
|
30
26
|
const notionKey = args["notion-key"];
|
|
31
|
-
const isGlobal = !!args.global;
|
|
32
27
|
const teamId = args["team-id"];
|
|
33
28
|
const projectId = args["project-id"];
|
|
34
29
|
const notionPage = args["notion-page"];
|
|
35
30
|
if (!linearKey) {
|
|
36
|
-
throw new Error("Usage: pm-skill init --linear-key <key> [--notion-key <key>]
|
|
31
|
+
throw new Error("Usage: npx pm-skill init --linear-key <key> [--notion-key <key>]\n" +
|
|
37
32
|
" --linear-key Linear API key (required)\n" +
|
|
38
33
|
" --notion-key Notion API key (optional)\n" +
|
|
39
|
-
" --global Save to ~/.pm-skill/ instead of CWD\n" +
|
|
40
34
|
" --team-id Linear team ID (auto-detected if omitted)\n" +
|
|
41
35
|
" --project-id Linear project ID (optional)\n" +
|
|
42
36
|
" --notion-page Notion root page ID (optional)");
|
|
43
37
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
console.log(`=== pm-skill init (${targetLabel}) ===\n`);
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
console.log(`=== pm-skill init ===\n`);
|
|
47
40
|
// 1. Validate Linear key
|
|
48
41
|
console.log("[Linear] Validating API key...");
|
|
49
42
|
const linearUser = await validateLinearKey(linearKey);
|
|
@@ -73,7 +66,7 @@ async function init(args) {
|
|
|
73
66
|
console.log(` Team: ${match.key} (${match.name})`);
|
|
74
67
|
}
|
|
75
68
|
else {
|
|
76
|
-
throw new Error(`Team '${teamId}' not found. Run 'pm-skill init --linear-key <key>' to see available teams.`);
|
|
69
|
+
throw new Error(`Team '${teamId}' not found. Run 'npx pm-skill init --linear-key <key>' to see available teams.`);
|
|
77
70
|
}
|
|
78
71
|
}
|
|
79
72
|
// 3. Validate Notion key (optional)
|
|
@@ -83,7 +76,7 @@ async function init(args) {
|
|
|
83
76
|
console.log(` Authenticated as: ${notionUser.name}`);
|
|
84
77
|
}
|
|
85
78
|
// 4. Write .env
|
|
86
|
-
console.log(
|
|
79
|
+
console.log("\n[Config] Writing .env...");
|
|
87
80
|
const envEntries = {
|
|
88
81
|
LINEAR_API_KEY: linearKey,
|
|
89
82
|
LINEAR_DEFAULT_TEAM_ID: selectedTeamId,
|
|
@@ -94,25 +87,26 @@ async function init(args) {
|
|
|
94
87
|
envEntries.NOTION_API_KEY = notionKey;
|
|
95
88
|
if (notionPage)
|
|
96
89
|
envEntries.NOTION_ROOT_PAGE_ID = notionPage;
|
|
97
|
-
const envPath = writeEnvFile(
|
|
90
|
+
const envPath = writeEnvFile(cwd, envEntries);
|
|
98
91
|
console.log(` Written: ${envPath}`);
|
|
99
|
-
// 5. Copy config.yml
|
|
100
|
-
console.log(
|
|
101
|
-
|
|
92
|
+
// 5. Copy config.yml, SKILL.md, AGENTS.md
|
|
93
|
+
console.log("\n[Files] Setting up project files...");
|
|
94
|
+
copyBundledFile("config.yml", resolve(cwd, "config.yml"));
|
|
95
|
+
copyBundledFile("SKILL.md", resolve(cwd, ".claude", "skills", "pm-skill", "SKILL.md"));
|
|
96
|
+
copyBundledFile("AGENTS.md", resolve(cwd, "AGENTS.md"));
|
|
102
97
|
// 6. Summary
|
|
103
98
|
console.log(`\n=== Init complete ===`);
|
|
104
|
-
console.log(` .env:
|
|
105
|
-
console.log(` config.yml:
|
|
106
|
-
console.log(`
|
|
107
|
-
console.log(`
|
|
99
|
+
console.log(` .env: ${envPath}`);
|
|
100
|
+
console.log(` config.yml: ${resolve(cwd, "config.yml")}`);
|
|
101
|
+
console.log(` SKILL.md: ${resolve(cwd, ".claude/skills/pm-skill/SKILL.md")}`);
|
|
102
|
+
console.log(` AGENTS.md: ${resolve(cwd, "AGENTS.md")}`);
|
|
103
|
+
console.log(` Linear: ${linearUser.name} | Team: ${selectedTeamId}`);
|
|
108
104
|
if (notionKey)
|
|
109
|
-
console.log(` Notion:
|
|
105
|
+
console.log(` Notion: connected`);
|
|
110
106
|
console.log(`\nNext steps:`);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
console.log(` - Customize config.yml labels/templates for your project`);
|
|
115
|
-
console.log(` - Run 'pm-skill setup' to verify label matching`);
|
|
107
|
+
console.log(` - Edit config.yml to match your project's labels/templates`);
|
|
108
|
+
console.log(` - Run 'npx pm-skill setup' to verify label matching`);
|
|
109
|
+
console.log(` - Run 'npx pm-skill setup --sync' to create missing labels in Linear`);
|
|
116
110
|
}
|
|
117
111
|
// ── Commands ──
|
|
118
112
|
async function setup(ctx, args) {
|
|
@@ -134,7 +128,7 @@ async function setup(ctx, args) {
|
|
|
134
128
|
}
|
|
135
129
|
// 3. Labels + matching
|
|
136
130
|
console.log("\n🏷️ Linear labels:");
|
|
137
|
-
|
|
131
|
+
const teamLabels = await getTeamLabels(ctx.linear, teamId);
|
|
138
132
|
for (const label of teamLabels) {
|
|
139
133
|
console.log(` ${label.name} | ${label.id}`);
|
|
140
134
|
}
|
|
@@ -165,29 +159,40 @@ async function setup(ctx, args) {
|
|
|
165
159
|
console.log("\nLabels synced successfully.");
|
|
166
160
|
}
|
|
167
161
|
else if (missingLabels.length > 0) {
|
|
168
|
-
console.log(`\n💡 ${missingLabels.length} label(s) missing. Run 'pm-skill setup --sync' to create them.`);
|
|
162
|
+
console.log(`\n💡 ${missingLabels.length} label(s) missing. Run 'npx pm-skill setup --sync' to create them.`);
|
|
169
163
|
}
|
|
170
164
|
else {
|
|
171
165
|
console.log("\n✅ All config labels matched.");
|
|
172
166
|
}
|
|
173
|
-
// 6.
|
|
174
|
-
console.log("\n
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
167
|
+
// 6. Notion connection check
|
|
168
|
+
console.log("\n📓 Notion:");
|
|
169
|
+
if (ctx.env.NOTION_API_KEY) {
|
|
170
|
+
try {
|
|
171
|
+
const notionInfo = await validateNotionKey(ctx.env.NOTION_API_KEY);
|
|
172
|
+
console.log(` ✅ Connected: ${notionInfo.name}`);
|
|
173
|
+
if (ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
174
|
+
console.log(` Root page: ${ctx.env.NOTION_ROOT_PAGE_ID}`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(` ⚠️ NOTION_ROOT_PAGE_ID not set — page creation will be skipped`);
|
|
178
|
+
}
|
|
179
|
+
if (ctx.env.NOTION_BUG_DB_ID) {
|
|
180
|
+
console.log(` Bug DB: ${ctx.env.NOTION_BUG_DB_ID}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
console.log(" ❌ Notion API key is invalid. Check https://www.notion.so/my-integrations");
|
|
185
|
+
}
|
|
179
186
|
}
|
|
180
187
|
else {
|
|
181
|
-
console.log("
|
|
182
|
-
|
|
183
|
-
if (!ctx.env.NOTION_API_KEY) {
|
|
184
|
-
console.log(" NOTION_API_KEY=<get from https://www.notion.so/my-integrations>");
|
|
188
|
+
console.log(" ⚠️ NOTION_API_KEY not set — Notion features disabled");
|
|
189
|
+
console.log(" Run 'npx pm-skill init --linear-key <key> --notion-key <key>' to set up");
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
async function startFeature(ctx, args) {
|
|
188
193
|
const title = args._[0];
|
|
189
194
|
if (!title) {
|
|
190
|
-
throw new Error("
|
|
195
|
+
throw new Error("Usage: pm-skill start-feature <title>");
|
|
191
196
|
}
|
|
192
197
|
const tmpl = getTemplate(ctx.config, "feature");
|
|
193
198
|
const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
|
|
@@ -196,7 +201,6 @@ async function startFeature(ctx, args) {
|
|
|
196
201
|
const priority = tmpl.linear_priority
|
|
197
202
|
? resolvePriority(ctx.config, tmpl.linear_priority)
|
|
198
203
|
: undefined;
|
|
199
|
-
// 1. Linear issue
|
|
200
204
|
const issue = await createIssue(ctx.linear, {
|
|
201
205
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
202
206
|
title,
|
|
@@ -204,25 +208,21 @@ async function startFeature(ctx, args) {
|
|
|
204
208
|
labelIds,
|
|
205
209
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
206
210
|
});
|
|
207
|
-
|
|
208
|
-
const issueUrl = issue.url;
|
|
209
|
-
console.log(`[Linear] 이슈 생성: ${issueId} — ${issueUrl}`);
|
|
210
|
-
// 2. Notion page
|
|
211
|
+
console.log(`[Linear] Issue created: ${issue.identifier} — ${issue.url}`);
|
|
211
212
|
if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
212
|
-
console.log("[Notion]
|
|
213
|
+
console.log("[Notion] Not configured — skipping page creation");
|
|
213
214
|
return;
|
|
214
215
|
}
|
|
215
|
-
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title,
|
|
216
|
-
console.log(`[Notion]
|
|
217
|
-
// 3. Link Linear → Notion
|
|
216
|
+
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url);
|
|
217
|
+
console.log(`[Notion] Page created: ${page.url}`);
|
|
218
218
|
await createAttachment(ctx.linear, issue.id, page.url, `${title} — PRD`);
|
|
219
|
-
console.log(`[Link] Linear ↔ Notion
|
|
220
|
-
console.log(`\n✅
|
|
219
|
+
console.log(`[Link] Linear ↔ Notion linked`);
|
|
220
|
+
console.log(`\n✅ Feature started: ${issue.identifier} | Notion: ${page.url}`);
|
|
221
221
|
}
|
|
222
222
|
async function reportBug(ctx, args) {
|
|
223
223
|
const title = args._[0];
|
|
224
224
|
if (!title) {
|
|
225
|
-
throw new Error("
|
|
225
|
+
throw new Error("Usage: pm-skill report-bug <title> [--severity high]");
|
|
226
226
|
}
|
|
227
227
|
const severity = args.severity ?? "medium";
|
|
228
228
|
const priority = resolveSeverity(ctx.config, severity);
|
|
@@ -230,7 +230,6 @@ async function reportBug(ctx, args) {
|
|
|
230
230
|
const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
|
|
231
231
|
const configLabels = tmpl.linear_labels.map((lid) => validateLabel(ctx.config, lid).name);
|
|
232
232
|
const labelIds = resolveLabels(configLabels, teamLabels);
|
|
233
|
-
// 1. Linear issue
|
|
234
233
|
const issue = await createIssue(ctx.linear, {
|
|
235
234
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
236
235
|
title,
|
|
@@ -238,43 +237,37 @@ async function reportBug(ctx, args) {
|
|
|
238
237
|
labelIds,
|
|
239
238
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
240
239
|
});
|
|
241
|
-
|
|
242
|
-
const issueUrl = issue.url;
|
|
243
|
-
console.log(`[Linear] 버그 이슈 생성: ${issueId} (severity: ${severity}) — ${issueUrl}`);
|
|
244
|
-
// 2. Notion
|
|
240
|
+
console.log(`[Linear] Bug created: ${issue.identifier} (severity: ${severity}) — ${issue.url}`);
|
|
245
241
|
if (!ctx.notion) {
|
|
246
|
-
console.log("[Notion]
|
|
242
|
+
console.log("[Notion] Not configured — skipping");
|
|
247
243
|
return;
|
|
248
244
|
}
|
|
249
245
|
let notionUrl;
|
|
250
246
|
if (ctx.env.NOTION_BUG_DB_ID) {
|
|
251
|
-
// DB 엔트리
|
|
252
247
|
const entry = await createDatabaseEntry(ctx.notion, ctx.env.NOTION_BUG_DB_ID, {
|
|
253
248
|
Name: { title: [{ text: { content: title } }] },
|
|
254
249
|
});
|
|
255
250
|
notionUrl = entry.url;
|
|
256
|
-
console.log(`[Notion]
|
|
251
|
+
console.log(`[Notion] Bug DB entry: ${notionUrl}`);
|
|
257
252
|
}
|
|
258
253
|
else if (ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
259
|
-
|
|
260
|
-
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issueUrl, severity);
|
|
254
|
+
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url, severity);
|
|
261
255
|
notionUrl = page.url;
|
|
262
|
-
console.log(`[Notion]
|
|
256
|
+
console.log(`[Notion] Bug report page: ${notionUrl}`);
|
|
263
257
|
}
|
|
264
258
|
else {
|
|
265
|
-
console.log("[Notion] NOTION_ROOT_PAGE_ID
|
|
259
|
+
console.log("[Notion] NOTION_ROOT_PAGE_ID not set — skipping");
|
|
266
260
|
return;
|
|
267
261
|
}
|
|
268
|
-
// 3. Link
|
|
269
262
|
await createAttachment(ctx.linear, issue.id, notionUrl, `${title} — Bug Report`);
|
|
270
|
-
console.log(`[Link] Linear ↔ Notion
|
|
271
|
-
console.log(`\n✅
|
|
263
|
+
console.log(`[Link] Linear ↔ Notion linked`);
|
|
264
|
+
console.log(`\n✅ Bug reported: ${issue.identifier} | Notion: ${notionUrl}`);
|
|
272
265
|
}
|
|
273
266
|
async function addTask(ctx, args) {
|
|
274
267
|
const parentIdentifier = args._[0];
|
|
275
268
|
const title = args._[1];
|
|
276
269
|
if (!parentIdentifier || !title) {
|
|
277
|
-
throw new Error("
|
|
270
|
+
throw new Error("Usage: pm-skill add-task <parent-issue> <title>");
|
|
278
271
|
}
|
|
279
272
|
const parent = await getIssue(ctx.linear, parentIdentifier);
|
|
280
273
|
const child = await createIssue(ctx.linear, {
|
|
@@ -283,33 +276,33 @@ async function addTask(ctx, args) {
|
|
|
283
276
|
parentId: parent.id,
|
|
284
277
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
285
278
|
});
|
|
286
|
-
console.log(`✅
|
|
279
|
+
console.log(`✅ Sub-issue created: ${child.identifier} (parent: ${parent.identifier})`);
|
|
287
280
|
}
|
|
288
281
|
async function relate(ctx, args) {
|
|
289
282
|
const id1 = args._[0];
|
|
290
283
|
const id2 = args._[1];
|
|
291
284
|
if (!id1 || !id2) {
|
|
292
|
-
throw new Error("
|
|
285
|
+
throw new Error("Usage: pm-skill relate <issue1> <issue2> [--type related]");
|
|
293
286
|
}
|
|
294
287
|
const type = args.type ?? "related";
|
|
295
288
|
if (type !== "related" && type !== "similar") {
|
|
296
|
-
throw new Error(`relate
|
|
289
|
+
throw new Error(`relate only supports 'related' or 'similar'. Use 'block' command for blocking relationships.`);
|
|
297
290
|
}
|
|
298
291
|
const issue1 = await getIssue(ctx.linear, id1);
|
|
299
292
|
const issue2 = await getIssue(ctx.linear, id2);
|
|
300
293
|
await createRelation(ctx.linear, issue1.id, issue2.id, type);
|
|
301
|
-
console.log(`✅ ${id1} ↔ ${id2} (${type})
|
|
294
|
+
console.log(`✅ ${id1} ↔ ${id2} (${type}) linked`);
|
|
302
295
|
}
|
|
303
296
|
async function block(ctx, args) {
|
|
304
297
|
const id1 = args._[0];
|
|
305
298
|
const id2 = args._[1];
|
|
306
299
|
if (!id1 || !id2) {
|
|
307
|
-
throw new Error("
|
|
300
|
+
throw new Error("Usage: pm-skill block <blocker> <blocked>");
|
|
308
301
|
}
|
|
309
302
|
const issue1 = await getIssue(ctx.linear, id1);
|
|
310
303
|
const issue2 = await getIssue(ctx.linear, id2);
|
|
311
304
|
await createRelation(ctx.linear, issue1.id, issue2.id, "blocks");
|
|
312
|
-
console.log(`✅ ${id1}
|
|
305
|
+
console.log(`✅ ${id1} blocks ${id2}`);
|
|
313
306
|
}
|
|
314
307
|
async function attachDoc(ctx, args) {
|
|
315
308
|
const identifier = args._[0];
|
|
@@ -317,17 +310,17 @@ async function attachDoc(ctx, args) {
|
|
|
317
310
|
const title = args.title;
|
|
318
311
|
const type = args.type;
|
|
319
312
|
if (!identifier || !url || !title || !type) {
|
|
320
|
-
throw new Error('
|
|
313
|
+
throw new Error('Usage: pm-skill attach-doc <issue> --url "URL" --title "Title" --type <type>');
|
|
321
314
|
}
|
|
322
315
|
validateDocType(ctx.config, type);
|
|
323
316
|
const issue = await getIssue(ctx.linear, identifier);
|
|
324
317
|
await createAttachment(ctx.linear, issue.id, url, title, type);
|
|
325
|
-
console.log(`✅ ${identifier}
|
|
318
|
+
console.log(`✅ ${identifier}: attached "${title}" (${type}) — ${url}`);
|
|
326
319
|
}
|
|
327
320
|
async function get(ctx, args) {
|
|
328
321
|
const identifier = args._[0];
|
|
329
322
|
if (!identifier) {
|
|
330
|
-
throw new Error("
|
|
323
|
+
throw new Error("Usage: pm-skill get <issue>");
|
|
331
324
|
}
|
|
332
325
|
const detail = await getIssueDetail(ctx.linear, identifier);
|
|
333
326
|
const { issue, children, relations, attachments } = detail;
|
|
@@ -335,26 +328,26 @@ async function get(ctx, args) {
|
|
|
335
328
|
const labels = await issue.labels();
|
|
336
329
|
const labelNames = labels.nodes.map((l) => l.name).join(", ");
|
|
337
330
|
console.log(`\n${issue.identifier}: ${issue.title}`);
|
|
338
|
-
console.log(
|
|
331
|
+
console.log(`State: ${state?.name ?? "?"} | Priority: ${issue.priority ?? "?"} | Labels: ${labelNames || "none"}`);
|
|
339
332
|
if (children.length > 0) {
|
|
340
|
-
console.log("\
|
|
333
|
+
console.log("\nSub-issues:");
|
|
341
334
|
for (const child of children) {
|
|
342
335
|
const childState = await child.state;
|
|
343
336
|
console.log(` ${child.identifier}: ${child.title} (${childState?.name ?? "?"})`);
|
|
344
337
|
}
|
|
345
338
|
}
|
|
346
339
|
if (relations.length > 0) {
|
|
347
|
-
console.log("\
|
|
340
|
+
console.log("\nRelations:");
|
|
348
341
|
for (const rel of relations) {
|
|
349
342
|
const arrow = rel.type === "blocks" ? "→ blocks" : "↔ related";
|
|
350
343
|
console.log(` ${arrow} ${rel.issue.identifier} (${rel.issue.title})`);
|
|
351
344
|
}
|
|
352
345
|
}
|
|
353
346
|
if (attachments.length > 0) {
|
|
354
|
-
console.log("\
|
|
347
|
+
console.log("\nAttachments:");
|
|
355
348
|
for (const att of attachments) {
|
|
356
349
|
const typeLabel = att.subtitle ? ` (${att.subtitle})` : "";
|
|
357
|
-
console.log(`
|
|
350
|
+
console.log(` ${att.title}${typeLabel} — ${att.url}`);
|
|
358
351
|
}
|
|
359
352
|
}
|
|
360
353
|
}
|
|
@@ -373,19 +366,25 @@ const COMMANDS = {
|
|
|
373
366
|
async function main() {
|
|
374
367
|
const args = minimist(process.argv.slice(2), {
|
|
375
368
|
string: ["severity", "type", "url", "title", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
376
|
-
boolean: ["
|
|
369
|
+
boolean: ["sync", "version"],
|
|
377
370
|
alias: { s: "severity", t: "type" },
|
|
378
371
|
});
|
|
372
|
+
// --version
|
|
373
|
+
if (args.version) {
|
|
374
|
+
const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, "package.json"), "utf-8"));
|
|
375
|
+
console.log(`pm-skill v${pkg.version}`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
379
378
|
const command = args._[0];
|
|
380
|
-
args._ = args._.slice(1);
|
|
379
|
+
args._ = args._.slice(1);
|
|
381
380
|
if (!command || command === "help") {
|
|
382
381
|
console.log(`pm-skill — Structured project management CLI (Linear + Notion)
|
|
383
382
|
|
|
384
|
-
Usage: pm-skill <command> [args] [flags]
|
|
383
|
+
Usage: npx pm-skill <command> [args] [flags]
|
|
385
384
|
|
|
386
385
|
Commands:
|
|
387
|
-
init --linear-key K [--notion-key K]
|
|
388
|
-
Initialize
|
|
386
|
+
init --linear-key K [--notion-key K]
|
|
387
|
+
Initialize project (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
|
|
389
388
|
setup [--sync] Verify config & label matching (--sync creates missing labels)
|
|
390
389
|
start-feature <title> Start feature (Linear issue + Notion PRD)
|
|
391
390
|
report-bug <title> [--severity S] File bug report (severity: urgent/high/medium/low)
|
|
@@ -396,8 +395,9 @@ Commands:
|
|
|
396
395
|
Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
|
|
397
396
|
get <issue> Show issue details
|
|
398
397
|
help Show this help
|
|
398
|
+
--version Show version
|
|
399
399
|
|
|
400
|
-
|
|
400
|
+
All config is per-project (CWD). Run 'npx pm-skill init' in each project.`);
|
|
401
401
|
return;
|
|
402
402
|
}
|
|
403
403
|
// init runs independently — no env/config validation
|
|
@@ -410,11 +410,8 @@ Config lookup: CWD/.env + ~/.pm-skill/.env (both loaded, CWD wins)`);
|
|
|
410
410
|
const available = ["init", ...Object.keys(COMMANDS)].join(", ");
|
|
411
411
|
throw new Error(`Unknown command: '${command}'\nAvailable: ${available}`);
|
|
412
412
|
}
|
|
413
|
-
// Validate env
|
|
414
413
|
const env = validateEnv(command);
|
|
415
|
-
// Load & validate config
|
|
416
414
|
const config = loadConfig();
|
|
417
|
-
// Build context
|
|
418
415
|
const linear = getLinearClient(env.LINEAR_API_KEY);
|
|
419
416
|
const notion = env.NOTION_API_KEY
|
|
420
417
|
? getNotionClient(env.NOTION_API_KEY)
|