pm-skill 1.0.2 → 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 +73 -94
- 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,7 +159,7 @@ 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.");
|
|
@@ -192,13 +186,13 @@ async function setup(ctx, args) {
|
|
|
192
186
|
}
|
|
193
187
|
else {
|
|
194
188
|
console.log(" ⚠️ NOTION_API_KEY not set — Notion features disabled");
|
|
195
|
-
console.log("
|
|
189
|
+
console.log(" Run 'npx pm-skill init --linear-key <key> --notion-key <key>' to set up");
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
async function startFeature(ctx, args) {
|
|
199
193
|
const title = args._[0];
|
|
200
194
|
if (!title) {
|
|
201
|
-
throw new Error("
|
|
195
|
+
throw new Error("Usage: pm-skill start-feature <title>");
|
|
202
196
|
}
|
|
203
197
|
const tmpl = getTemplate(ctx.config, "feature");
|
|
204
198
|
const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
|
|
@@ -207,7 +201,6 @@ async function startFeature(ctx, args) {
|
|
|
207
201
|
const priority = tmpl.linear_priority
|
|
208
202
|
? resolvePriority(ctx.config, tmpl.linear_priority)
|
|
209
203
|
: undefined;
|
|
210
|
-
// 1. Linear issue
|
|
211
204
|
const issue = await createIssue(ctx.linear, {
|
|
212
205
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
213
206
|
title,
|
|
@@ -215,25 +208,21 @@ async function startFeature(ctx, args) {
|
|
|
215
208
|
labelIds,
|
|
216
209
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
217
210
|
});
|
|
218
|
-
|
|
219
|
-
const issueUrl = issue.url;
|
|
220
|
-
console.log(`[Linear] 이슈 생성: ${issueId} — ${issueUrl}`);
|
|
221
|
-
// 2. Notion page
|
|
211
|
+
console.log(`[Linear] Issue created: ${issue.identifier} — ${issue.url}`);
|
|
222
212
|
if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
223
|
-
console.log("[Notion]
|
|
213
|
+
console.log("[Notion] Not configured — skipping page creation");
|
|
224
214
|
return;
|
|
225
215
|
}
|
|
226
|
-
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title,
|
|
227
|
-
console.log(`[Notion]
|
|
228
|
-
// 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}`);
|
|
229
218
|
await createAttachment(ctx.linear, issue.id, page.url, `${title} — PRD`);
|
|
230
|
-
console.log(`[Link] Linear ↔ Notion
|
|
231
|
-
console.log(`\n✅
|
|
219
|
+
console.log(`[Link] Linear ↔ Notion linked`);
|
|
220
|
+
console.log(`\n✅ Feature started: ${issue.identifier} | Notion: ${page.url}`);
|
|
232
221
|
}
|
|
233
222
|
async function reportBug(ctx, args) {
|
|
234
223
|
const title = args._[0];
|
|
235
224
|
if (!title) {
|
|
236
|
-
throw new Error("
|
|
225
|
+
throw new Error("Usage: pm-skill report-bug <title> [--severity high]");
|
|
237
226
|
}
|
|
238
227
|
const severity = args.severity ?? "medium";
|
|
239
228
|
const priority = resolveSeverity(ctx.config, severity);
|
|
@@ -241,7 +230,6 @@ async function reportBug(ctx, args) {
|
|
|
241
230
|
const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
|
|
242
231
|
const configLabels = tmpl.linear_labels.map((lid) => validateLabel(ctx.config, lid).name);
|
|
243
232
|
const labelIds = resolveLabels(configLabels, teamLabels);
|
|
244
|
-
// 1. Linear issue
|
|
245
233
|
const issue = await createIssue(ctx.linear, {
|
|
246
234
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
247
235
|
title,
|
|
@@ -249,43 +237,37 @@ async function reportBug(ctx, args) {
|
|
|
249
237
|
labelIds,
|
|
250
238
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
251
239
|
});
|
|
252
|
-
|
|
253
|
-
const issueUrl = issue.url;
|
|
254
|
-
console.log(`[Linear] 버그 이슈 생성: ${issueId} (severity: ${severity}) — ${issueUrl}`);
|
|
255
|
-
// 2. Notion
|
|
240
|
+
console.log(`[Linear] Bug created: ${issue.identifier} (severity: ${severity}) — ${issue.url}`);
|
|
256
241
|
if (!ctx.notion) {
|
|
257
|
-
console.log("[Notion]
|
|
242
|
+
console.log("[Notion] Not configured — skipping");
|
|
258
243
|
return;
|
|
259
244
|
}
|
|
260
245
|
let notionUrl;
|
|
261
246
|
if (ctx.env.NOTION_BUG_DB_ID) {
|
|
262
|
-
// DB 엔트리
|
|
263
247
|
const entry = await createDatabaseEntry(ctx.notion, ctx.env.NOTION_BUG_DB_ID, {
|
|
264
248
|
Name: { title: [{ text: { content: title } }] },
|
|
265
249
|
});
|
|
266
250
|
notionUrl = entry.url;
|
|
267
|
-
console.log(`[Notion]
|
|
251
|
+
console.log(`[Notion] Bug DB entry: ${notionUrl}`);
|
|
268
252
|
}
|
|
269
253
|
else if (ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
270
|
-
|
|
271
|
-
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);
|
|
272
255
|
notionUrl = page.url;
|
|
273
|
-
console.log(`[Notion]
|
|
256
|
+
console.log(`[Notion] Bug report page: ${notionUrl}`);
|
|
274
257
|
}
|
|
275
258
|
else {
|
|
276
|
-
console.log("[Notion] NOTION_ROOT_PAGE_ID
|
|
259
|
+
console.log("[Notion] NOTION_ROOT_PAGE_ID not set — skipping");
|
|
277
260
|
return;
|
|
278
261
|
}
|
|
279
|
-
// 3. Link
|
|
280
262
|
await createAttachment(ctx.linear, issue.id, notionUrl, `${title} — Bug Report`);
|
|
281
|
-
console.log(`[Link] Linear ↔ Notion
|
|
282
|
-
console.log(`\n✅
|
|
263
|
+
console.log(`[Link] Linear ↔ Notion linked`);
|
|
264
|
+
console.log(`\n✅ Bug reported: ${issue.identifier} | Notion: ${notionUrl}`);
|
|
283
265
|
}
|
|
284
266
|
async function addTask(ctx, args) {
|
|
285
267
|
const parentIdentifier = args._[0];
|
|
286
268
|
const title = args._[1];
|
|
287
269
|
if (!parentIdentifier || !title) {
|
|
288
|
-
throw new Error("
|
|
270
|
+
throw new Error("Usage: pm-skill add-task <parent-issue> <title>");
|
|
289
271
|
}
|
|
290
272
|
const parent = await getIssue(ctx.linear, parentIdentifier);
|
|
291
273
|
const child = await createIssue(ctx.linear, {
|
|
@@ -294,33 +276,33 @@ async function addTask(ctx, args) {
|
|
|
294
276
|
parentId: parent.id,
|
|
295
277
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
296
278
|
});
|
|
297
|
-
console.log(`✅
|
|
279
|
+
console.log(`✅ Sub-issue created: ${child.identifier} (parent: ${parent.identifier})`);
|
|
298
280
|
}
|
|
299
281
|
async function relate(ctx, args) {
|
|
300
282
|
const id1 = args._[0];
|
|
301
283
|
const id2 = args._[1];
|
|
302
284
|
if (!id1 || !id2) {
|
|
303
|
-
throw new Error("
|
|
285
|
+
throw new Error("Usage: pm-skill relate <issue1> <issue2> [--type related]");
|
|
304
286
|
}
|
|
305
287
|
const type = args.type ?? "related";
|
|
306
288
|
if (type !== "related" && type !== "similar") {
|
|
307
|
-
throw new Error(`relate
|
|
289
|
+
throw new Error(`relate only supports 'related' or 'similar'. Use 'block' command for blocking relationships.`);
|
|
308
290
|
}
|
|
309
291
|
const issue1 = await getIssue(ctx.linear, id1);
|
|
310
292
|
const issue2 = await getIssue(ctx.linear, id2);
|
|
311
293
|
await createRelation(ctx.linear, issue1.id, issue2.id, type);
|
|
312
|
-
console.log(`✅ ${id1} ↔ ${id2} (${type})
|
|
294
|
+
console.log(`✅ ${id1} ↔ ${id2} (${type}) linked`);
|
|
313
295
|
}
|
|
314
296
|
async function block(ctx, args) {
|
|
315
297
|
const id1 = args._[0];
|
|
316
298
|
const id2 = args._[1];
|
|
317
299
|
if (!id1 || !id2) {
|
|
318
|
-
throw new Error("
|
|
300
|
+
throw new Error("Usage: pm-skill block <blocker> <blocked>");
|
|
319
301
|
}
|
|
320
302
|
const issue1 = await getIssue(ctx.linear, id1);
|
|
321
303
|
const issue2 = await getIssue(ctx.linear, id2);
|
|
322
304
|
await createRelation(ctx.linear, issue1.id, issue2.id, "blocks");
|
|
323
|
-
console.log(`✅ ${id1}
|
|
305
|
+
console.log(`✅ ${id1} blocks ${id2}`);
|
|
324
306
|
}
|
|
325
307
|
async function attachDoc(ctx, args) {
|
|
326
308
|
const identifier = args._[0];
|
|
@@ -328,17 +310,17 @@ async function attachDoc(ctx, args) {
|
|
|
328
310
|
const title = args.title;
|
|
329
311
|
const type = args.type;
|
|
330
312
|
if (!identifier || !url || !title || !type) {
|
|
331
|
-
throw new Error('
|
|
313
|
+
throw new Error('Usage: pm-skill attach-doc <issue> --url "URL" --title "Title" --type <type>');
|
|
332
314
|
}
|
|
333
315
|
validateDocType(ctx.config, type);
|
|
334
316
|
const issue = await getIssue(ctx.linear, identifier);
|
|
335
317
|
await createAttachment(ctx.linear, issue.id, url, title, type);
|
|
336
|
-
console.log(`✅ ${identifier}
|
|
318
|
+
console.log(`✅ ${identifier}: attached "${title}" (${type}) — ${url}`);
|
|
337
319
|
}
|
|
338
320
|
async function get(ctx, args) {
|
|
339
321
|
const identifier = args._[0];
|
|
340
322
|
if (!identifier) {
|
|
341
|
-
throw new Error("
|
|
323
|
+
throw new Error("Usage: pm-skill get <issue>");
|
|
342
324
|
}
|
|
343
325
|
const detail = await getIssueDetail(ctx.linear, identifier);
|
|
344
326
|
const { issue, children, relations, attachments } = detail;
|
|
@@ -346,26 +328,26 @@ async function get(ctx, args) {
|
|
|
346
328
|
const labels = await issue.labels();
|
|
347
329
|
const labelNames = labels.nodes.map((l) => l.name).join(", ");
|
|
348
330
|
console.log(`\n${issue.identifier}: ${issue.title}`);
|
|
349
|
-
console.log(
|
|
331
|
+
console.log(`State: ${state?.name ?? "?"} | Priority: ${issue.priority ?? "?"} | Labels: ${labelNames || "none"}`);
|
|
350
332
|
if (children.length > 0) {
|
|
351
|
-
console.log("\
|
|
333
|
+
console.log("\nSub-issues:");
|
|
352
334
|
for (const child of children) {
|
|
353
335
|
const childState = await child.state;
|
|
354
336
|
console.log(` ${child.identifier}: ${child.title} (${childState?.name ?? "?"})`);
|
|
355
337
|
}
|
|
356
338
|
}
|
|
357
339
|
if (relations.length > 0) {
|
|
358
|
-
console.log("\
|
|
340
|
+
console.log("\nRelations:");
|
|
359
341
|
for (const rel of relations) {
|
|
360
342
|
const arrow = rel.type === "blocks" ? "→ blocks" : "↔ related";
|
|
361
343
|
console.log(` ${arrow} ${rel.issue.identifier} (${rel.issue.title})`);
|
|
362
344
|
}
|
|
363
345
|
}
|
|
364
346
|
if (attachments.length > 0) {
|
|
365
|
-
console.log("\
|
|
347
|
+
console.log("\nAttachments:");
|
|
366
348
|
for (const att of attachments) {
|
|
367
349
|
const typeLabel = att.subtitle ? ` (${att.subtitle})` : "";
|
|
368
|
-
console.log(`
|
|
350
|
+
console.log(` ${att.title}${typeLabel} — ${att.url}`);
|
|
369
351
|
}
|
|
370
352
|
}
|
|
371
353
|
}
|
|
@@ -384,26 +366,25 @@ const COMMANDS = {
|
|
|
384
366
|
async function main() {
|
|
385
367
|
const args = minimist(process.argv.slice(2), {
|
|
386
368
|
string: ["severity", "type", "url", "title", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
387
|
-
boolean: ["
|
|
369
|
+
boolean: ["sync", "version"],
|
|
388
370
|
alias: { s: "severity", t: "type" },
|
|
389
371
|
});
|
|
390
372
|
// --version
|
|
391
373
|
if (args.version) {
|
|
392
|
-
const { readFileSync } = await import("fs");
|
|
393
374
|
const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, "package.json"), "utf-8"));
|
|
394
375
|
console.log(`pm-skill v${pkg.version}`);
|
|
395
376
|
return;
|
|
396
377
|
}
|
|
397
378
|
const command = args._[0];
|
|
398
|
-
args._ = args._.slice(1);
|
|
379
|
+
args._ = args._.slice(1);
|
|
399
380
|
if (!command || command === "help") {
|
|
400
381
|
console.log(`pm-skill — Structured project management CLI (Linear + Notion)
|
|
401
382
|
|
|
402
|
-
Usage: pm-skill <command> [args] [flags]
|
|
383
|
+
Usage: npx pm-skill <command> [args] [flags]
|
|
403
384
|
|
|
404
385
|
Commands:
|
|
405
|
-
init --linear-key K [--notion-key K]
|
|
406
|
-
Initialize
|
|
386
|
+
init --linear-key K [--notion-key K]
|
|
387
|
+
Initialize project (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
|
|
407
388
|
setup [--sync] Verify config & label matching (--sync creates missing labels)
|
|
408
389
|
start-feature <title> Start feature (Linear issue + Notion PRD)
|
|
409
390
|
report-bug <title> [--severity S] File bug report (severity: urgent/high/medium/low)
|
|
@@ -414,8 +395,9 @@ Commands:
|
|
|
414
395
|
Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
|
|
415
396
|
get <issue> Show issue details
|
|
416
397
|
help Show this help
|
|
398
|
+
--version Show version
|
|
417
399
|
|
|
418
|
-
|
|
400
|
+
All config is per-project (CWD). Run 'npx pm-skill init' in each project.`);
|
|
419
401
|
return;
|
|
420
402
|
}
|
|
421
403
|
// init runs independently — no env/config validation
|
|
@@ -428,11 +410,8 @@ Config lookup: CWD/.env + ~/.pm-skill/.env (both loaded, CWD wins)`);
|
|
|
428
410
|
const available = ["init", ...Object.keys(COMMANDS)].join(", ");
|
|
429
411
|
throw new Error(`Unknown command: '${command}'\nAvailable: ${available}`);
|
|
430
412
|
}
|
|
431
|
-
// Validate env
|
|
432
413
|
const env = validateEnv(command);
|
|
433
|
-
// Load & validate config
|
|
434
414
|
const config = loadConfig();
|
|
435
|
-
// Build context
|
|
436
415
|
const linear = getLinearClient(env.LINEAR_API_KEY);
|
|
437
416
|
const notion = env.NOTION_API_KEY
|
|
438
417
|
? getNotionClient(env.NOTION_API_KEY)
|