pm-skill 1.1.4 → 1.1.6
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/.env.example +14 -6
- package/AGENTS.md +87 -6
- package/README.md +10 -0
- package/SKILL.md +68 -19
- package/config.yml +5 -19
- package/dist/config.d.ts +0 -1
- package/dist/config.js +2 -2
- package/dist/env.js +0 -8
- package/dist/notion.d.ts +0 -18
- package/dist/notion.js +0 -186
- package/dist/workflows.js +140 -63
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
# === Linear ===
|
|
2
|
-
# Settings > API > Personal API Keys
|
|
2
|
+
# Get from: Linear > Settings > API > Personal API Keys
|
|
3
|
+
# 발급: Linear > Settings > API > Personal API Keys
|
|
3
4
|
LINEAR_API_KEY=lin_api_xxxxxxxx
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
+
# Discovered by `npx pm-skill init` (auto-detected)
|
|
7
|
+
# `npx pm-skill init`으로 자동 감지됨
|
|
6
8
|
LINEAR_DEFAULT_TEAM_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
9
|
+
|
|
10
|
+
# Optional — discovered by `npx pm-skill init` or `select-project`
|
|
11
|
+
# 선택사항 — init 또는 select-project로 자동 감지
|
|
7
12
|
LINEAR_DEFAULT_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
8
13
|
|
|
9
|
-
# === Notion ===
|
|
10
|
-
# https://www.notion.so/my-integrations
|
|
14
|
+
# === Notion (optional) ===
|
|
15
|
+
# Get from: https://www.notion.so/my-integrations > New integration
|
|
16
|
+
# 발급: https://www.notion.so/my-integrations > New integration
|
|
11
17
|
NOTION_API_KEY=secret_xxxxxxxx
|
|
12
18
|
|
|
13
|
-
#
|
|
19
|
+
# Parent page where docs are created — discovered by `npx pm-skill init`
|
|
20
|
+
# 문서가 생성될 상위 페이지 — init으로 자동 감지
|
|
14
21
|
NOTION_ROOT_PAGE_ID=xxxxxxxx
|
|
15
22
|
|
|
16
|
-
#
|
|
23
|
+
# Optional — bug tracking DB ID (if omitted, bugs use pages instead)
|
|
24
|
+
# 선택사항 — 버그 추적 DB ID (미설정 시 페이지로 생성)
|
|
17
25
|
NOTION_BUG_DB_ID=xxxxxxxx
|
package/AGENTS.md
CHANGED
|
@@ -6,6 +6,38 @@ This file provides instructions for AI coding assistants (Codex, Claude Code, et
|
|
|
6
6
|
|
|
7
7
|
A structured project management CLI that integrates Linear (issue tracking) and Notion (documentation). It enforces a config-driven workflow where only pre-defined labels, templates, and severity levels are allowed.
|
|
8
8
|
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
- If the user writes `/pm-skill <args>`, execute `npx pm-skill <args>` from the project root.
|
|
12
|
+
- When the user mentions pm-skill, Linear issues, or Notion documents, prefer using pm-skill commands.
|
|
13
|
+
- This tool requires shell execution and network access (Linear/Notion APIs).
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- Node.js 18 or higher (`node --version` to check)
|
|
18
|
+
- Linear API key (get from: Linear > Settings > API > Personal API Keys)
|
|
19
|
+
- Notion API key (optional, get from: https://www.notion.so/my-integrations)
|
|
20
|
+
|
|
21
|
+
## First-Time Setup
|
|
22
|
+
|
|
23
|
+
If `.env` does not exist in the project root, run initialization first:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Step 1: Initialize (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
|
|
27
|
+
npx pm-skill init --linear-key <LINEAR_API_KEY> --notion-key <NOTION_API_KEY>
|
|
28
|
+
|
|
29
|
+
# Step 2: Verify setup and label matching
|
|
30
|
+
npx pm-skill setup
|
|
31
|
+
|
|
32
|
+
# Step 3 (optional): Create missing labels in Linear
|
|
33
|
+
npx pm-skill setup --sync
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If init fails:
|
|
37
|
+
- **"Linear API key validation failed"** — check the key at Linear > Settings > API
|
|
38
|
+
- **"No pages shared"** — share a Notion page with the integration first (page menu > Connections)
|
|
39
|
+
- **Network error** — check internet connection and retry
|
|
40
|
+
|
|
9
41
|
## How to Run Commands
|
|
10
42
|
|
|
11
43
|
```bash
|
|
@@ -21,8 +53,22 @@ npx pm-skill setup
|
|
|
21
53
|
npx pm-skill setup --sync # create missing labels in Linear
|
|
22
54
|
```
|
|
23
55
|
|
|
56
|
+
### select-project
|
|
57
|
+
List or switch the active Linear project.
|
|
58
|
+
```bash
|
|
59
|
+
npx pm-skill select-project # list projects
|
|
60
|
+
npx pm-skill select-project "Project Name" # switch
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### select-page
|
|
64
|
+
List or switch the active Notion root page.
|
|
65
|
+
```bash
|
|
66
|
+
npx pm-skill select-page # list pages
|
|
67
|
+
npx pm-skill select-page "Page Name" # switch
|
|
68
|
+
```
|
|
69
|
+
|
|
24
70
|
### start-feature
|
|
25
|
-
Create a Linear issue
|
|
71
|
+
Create a Linear issue with a task checklist.
|
|
26
72
|
```bash
|
|
27
73
|
npx pm-skill start-feature "<title>"
|
|
28
74
|
```
|
|
@@ -45,7 +91,6 @@ Set a relationship between two issues.
|
|
|
45
91
|
```bash
|
|
46
92
|
npx pm-skill relate <issue1> <issue2> --type <related|similar>
|
|
47
93
|
```
|
|
48
|
-
Default type: related.
|
|
49
94
|
|
|
50
95
|
### block
|
|
51
96
|
Set a blocking dependency (issue1 blocks issue2).
|
|
@@ -53,6 +98,30 @@ Set a blocking dependency (issue1 blocks issue2).
|
|
|
53
98
|
npx pm-skill block <blocker-issue> <blocked-issue>
|
|
54
99
|
```
|
|
55
100
|
|
|
101
|
+
### push-doc
|
|
102
|
+
Upload markdown to Notion. Optionally link to a Linear issue.
|
|
103
|
+
```bash
|
|
104
|
+
# From file
|
|
105
|
+
npx pm-skill push-doc ./doc.md --title "Title" --parent <page-id> --issue <issue-id>
|
|
106
|
+
|
|
107
|
+
# From content (AI agent use case)
|
|
108
|
+
npx pm-skill push-doc --title "Title" --content "# Markdown..." --issue <issue-id>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### update-doc
|
|
112
|
+
Replace existing Notion page content with new markdown.
|
|
113
|
+
```bash
|
|
114
|
+
npx pm-skill update-doc <page-id> ./updated.md
|
|
115
|
+
npx pm-skill update-doc <page-id> --content "# Updated..."
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### create-folder
|
|
119
|
+
Create an empty Notion page as a category/folder.
|
|
120
|
+
```bash
|
|
121
|
+
npx pm-skill create-folder "Folder Name" --parent <page-id>
|
|
122
|
+
# Returns page ID — use with push-doc --parent
|
|
123
|
+
```
|
|
124
|
+
|
|
56
125
|
### attach-doc
|
|
57
126
|
Attach a document URL to an issue with type validation.
|
|
58
127
|
```bash
|
|
@@ -65,21 +134,33 @@ Show issue details including children, relations, and attachments.
|
|
|
65
134
|
npx pm-skill get <issue-id>
|
|
66
135
|
```
|
|
67
136
|
|
|
137
|
+
### delete
|
|
138
|
+
Delete issue(s) and their linked Notion pages.
|
|
139
|
+
```bash
|
|
140
|
+
npx pm-skill delete <issue-id>
|
|
141
|
+
npx pm-skill delete <issue-id> --recursive # also delete sub-issues
|
|
142
|
+
```
|
|
143
|
+
|
|
68
144
|
## Configuration
|
|
69
145
|
|
|
70
146
|
All config is per-project (CWD):
|
|
71
|
-
- **`.env`** — API keys and IDs
|
|
147
|
+
- **`.env`** — API keys and IDs (never commit this file)
|
|
72
148
|
- **`config.yml`** — Labels, templates, priorities, severity mappings
|
|
73
149
|
|
|
74
150
|
## Workflow Patterns
|
|
75
151
|
|
|
76
152
|
### Feature Development
|
|
77
|
-
1. `npx pm-skill start-feature "Feature Name"` — creates Linear issue
|
|
153
|
+
1. `npx pm-skill start-feature "Feature Name"` — creates Linear issue with checklist
|
|
78
154
|
2. `npx pm-skill add-task ENG-XX "Sub-task 1"` — break down into sub-tasks
|
|
79
|
-
3. `npx pm-skill
|
|
80
|
-
4. `npx pm-skill
|
|
155
|
+
3. `npx pm-skill push-doc ./design.md --issue ENG-XX` — upload docs when ready
|
|
156
|
+
4. `npx pm-skill relate ENG-XX ENG-YY` — link related issues
|
|
81
157
|
|
|
82
158
|
### Bug Fix
|
|
83
159
|
1. `npx pm-skill report-bug "Bug Description" --severity high`
|
|
84
160
|
2. `npx pm-skill add-task ENG-XX "Root cause analysis"`
|
|
85
161
|
3. `npx pm-skill add-task ENG-XX "Fix and test"`
|
|
162
|
+
|
|
163
|
+
### Document Management
|
|
164
|
+
1. `npx pm-skill create-folder "Schema Docs"` — create category
|
|
165
|
+
2. `npx pm-skill push-doc ./schema.md --parent <folder-id>` — upload under category
|
|
166
|
+
3. `npx pm-skill update-doc <page-id> ./schema-v2.md` — update later
|
package/README.md
CHANGED
|
@@ -15,6 +15,12 @@ Structured project management CLI that integrates **Linear** and **Notion**. Des
|
|
|
15
15
|
- **attach-doc** — Attach documents with type validation
|
|
16
16
|
- **get** — View issue details with children, relations, and attachments
|
|
17
17
|
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- **Node.js 18+** — check with `node --version`
|
|
21
|
+
- **Linear API key** — Linear > Settings > API > Personal API Keys
|
|
22
|
+
- **Notion API key** (optional) — https://www.notion.so/my-integrations
|
|
23
|
+
|
|
18
24
|
## Quick Start
|
|
19
25
|
|
|
20
26
|
```bash
|
|
@@ -122,6 +128,10 @@ npx pm-skill start-feature "My Feature"
|
|
|
122
128
|
| `doc_types` | Document types for attach-doc validation |
|
|
123
129
|
| `epics` | Epic definitions (project-specific) |
|
|
124
130
|
|
|
131
|
+
## Security
|
|
132
|
+
|
|
133
|
+
`.env` contains API keys — **never commit it to git**. The `init` command creates `.env` locally, and it's already in `.gitignore`.
|
|
134
|
+
|
|
125
135
|
## Per-Project Model
|
|
126
136
|
|
|
127
137
|
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.
|
package/SKILL.md
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pm-skill
|
|
3
|
+
description: Structured project management CLI — Linear + Notion integration. Trigger on: pm-skill, Linear issue, Notion doc, start-feature, report-bug, push-doc, backlog
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# PM Skill — Structured Project Management
|
|
2
7
|
|
|
3
8
|
Linear + Notion integration for structured project management.
|
|
4
9
|
"Design freedom, usage discipline" — only labels/templates/severity defined in `config.yml` are allowed.
|
|
5
10
|
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
- When the user mentions **pm-skill**, **Linear issue**, **Notion document**, **start-feature**, **report-bug**, **backlog**, or **push-doc**, use this skill.
|
|
14
|
+
- If the user writes `/pm-skill <args>`, execute `npx pm-skill <args>` from the project root.
|
|
15
|
+
- This skill requires **shell execution** and **network access** (Linear/Notion APIs).
|
|
16
|
+
- If `.env` does not exist, run `npx pm-skill init` first.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
Requires Node.js 18+. If `.env` does not exist in the project, run `init` first.
|
|
21
|
+
|
|
6
22
|
## Setup
|
|
7
23
|
|
|
8
24
|
```bash
|
|
@@ -14,6 +30,9 @@ npx pm-skill setup
|
|
|
14
30
|
|
|
15
31
|
# Create missing labels in Linear
|
|
16
32
|
npx pm-skill setup --sync
|
|
33
|
+
|
|
34
|
+
# Install as Codex global skill (optional)
|
|
35
|
+
npx pm-skill install-codex-skill
|
|
17
36
|
```
|
|
18
37
|
|
|
19
38
|
## Commands
|
|
@@ -24,8 +43,15 @@ Verify Linear/Notion connection + label matching. `--sync` creates missing label
|
|
|
24
43
|
npx pm-skill setup
|
|
25
44
|
```
|
|
26
45
|
|
|
46
|
+
### select-project / select-page
|
|
47
|
+
Switch active Linear project or Notion root page.
|
|
48
|
+
```bash
|
|
49
|
+
npx pm-skill select-project "Project Name"
|
|
50
|
+
npx pm-skill select-page "Page Name"
|
|
51
|
+
```
|
|
52
|
+
|
|
27
53
|
### start-feature
|
|
28
|
-
Start feature development. Creates Linear issue
|
|
54
|
+
Start feature development. Creates Linear issue with task checklist.
|
|
29
55
|
```bash
|
|
30
56
|
npx pm-skill start-feature "Feature title"
|
|
31
57
|
```
|
|
@@ -43,20 +69,41 @@ Add sub-task to an issue.
|
|
|
43
69
|
npx pm-skill add-task ENG-10 "Write unit tests"
|
|
44
70
|
```
|
|
45
71
|
|
|
46
|
-
### relate
|
|
47
|
-
Link
|
|
72
|
+
### relate / block
|
|
73
|
+
Link or set blocking relationship between issues.
|
|
48
74
|
```bash
|
|
49
75
|
npx pm-skill relate ENG-10 ENG-11 --type related
|
|
76
|
+
npx pm-skill block ENG-10 ENG-11
|
|
50
77
|
```
|
|
51
78
|
|
|
52
|
-
###
|
|
53
|
-
|
|
79
|
+
### push-doc
|
|
80
|
+
Upload markdown to Notion. Optionally link to a Linear issue.
|
|
54
81
|
```bash
|
|
55
|
-
|
|
82
|
+
# From file
|
|
83
|
+
npx pm-skill push-doc ./design.md --title "Design Doc" --issue ENG-10
|
|
84
|
+
|
|
85
|
+
# From content (AI agent use case)
|
|
86
|
+
npx pm-skill push-doc --title "Report" --content "# Results..." --issue ENG-10
|
|
87
|
+
|
|
88
|
+
# Under a specific parent page
|
|
89
|
+
npx pm-skill push-doc ./schema.md --parent <page-id>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### update-doc
|
|
93
|
+
Replace existing Notion page content with new markdown.
|
|
94
|
+
```bash
|
|
95
|
+
npx pm-skill update-doc <page-id> ./updated.md
|
|
96
|
+
npx pm-skill update-doc <page-id> --content "# Updated..."
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### create-folder
|
|
100
|
+
Create Notion page as a category/folder.
|
|
101
|
+
```bash
|
|
102
|
+
npx pm-skill create-folder "Schema Docs" --parent <page-id>
|
|
56
103
|
```
|
|
57
104
|
|
|
58
105
|
### attach-doc
|
|
59
|
-
Attach document URL to issue
|
|
106
|
+
Attach a document URL to an issue with type validation.
|
|
60
107
|
```bash
|
|
61
108
|
npx pm-skill attach-doc ENG-10 \
|
|
62
109
|
--url "https://notion.so/..." \
|
|
@@ -71,6 +118,13 @@ Show issue details including sub-issues, relations, and attachments.
|
|
|
71
118
|
npx pm-skill get ENG-10
|
|
72
119
|
```
|
|
73
120
|
|
|
121
|
+
### delete
|
|
122
|
+
Delete issue(s) and linked Notion pages.
|
|
123
|
+
```bash
|
|
124
|
+
npx pm-skill delete ENG-10
|
|
125
|
+
npx pm-skill delete ENG-10 --recursive # also delete sub-issues
|
|
126
|
+
```
|
|
127
|
+
|
|
74
128
|
## Workflow Examples
|
|
75
129
|
|
|
76
130
|
### Feature Development
|
|
@@ -78,9 +132,8 @@ npx pm-skill get ENG-10
|
|
|
78
132
|
npx pm-skill start-feature "Booking cancellation"
|
|
79
133
|
npx pm-skill add-task ENG-10 "API endpoint"
|
|
80
134
|
npx pm-skill add-task ENG-10 "Frontend UI"
|
|
81
|
-
npx pm-skill
|
|
135
|
+
npx pm-skill push-doc ./design.md --issue ENG-10
|
|
82
136
|
npx pm-skill relate ENG-10 ENG-8 --type related
|
|
83
|
-
npx pm-skill block ENG-10 ENG-15
|
|
84
137
|
```
|
|
85
138
|
|
|
86
139
|
### Bug Fix
|
|
@@ -90,13 +143,9 @@ npx pm-skill add-task ENG-20 "Root cause analysis"
|
|
|
90
143
|
npx pm-skill add-task ENG-20 "Fix and test"
|
|
91
144
|
```
|
|
92
145
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 |
|
|
146
|
+
### Document Management
|
|
147
|
+
```bash
|
|
148
|
+
npx pm-skill create-folder "Schema Docs"
|
|
149
|
+
npx pm-skill push-doc ./schema.md --parent <folder-id>
|
|
150
|
+
npx pm-skill update-doc <page-id> ./schema-v2.md
|
|
151
|
+
```
|
package/config.yml
CHANGED
|
@@ -19,43 +19,29 @@ labels:
|
|
|
19
19
|
name: Refactor
|
|
20
20
|
description: "기존 코드 리팩토링 (기능 변경 없음)"
|
|
21
21
|
color: "#888888"
|
|
22
|
-
- id: admin
|
|
23
|
-
name: Admin
|
|
24
|
-
description: "인프라, 배포, 환경 설정 등 관리 작업"
|
|
25
|
-
color: "#ffaa00"
|
|
26
|
-
- id: guest
|
|
27
|
-
name: Guest
|
|
28
|
-
description: "외부 의존성, 서드파티 연동 관련"
|
|
29
|
-
color: "#aa88ff"
|
|
30
|
-
|
|
31
22
|
templates:
|
|
32
23
|
- id: feature
|
|
33
|
-
name: Feature
|
|
34
|
-
description: "기능
|
|
35
|
-
notion_template: feature-prd
|
|
24
|
+
name: Feature
|
|
25
|
+
description: "기능 개발 템플릿"
|
|
36
26
|
linear_labels: [dev]
|
|
37
27
|
linear_priority: p2
|
|
38
28
|
- id: bugfix
|
|
39
|
-
name: Bug
|
|
40
|
-
description: "버그
|
|
41
|
-
notion_template: bug-report
|
|
29
|
+
name: Bug Fix
|
|
30
|
+
description: "버그 수정 템플릿"
|
|
42
31
|
linear_labels: [bug]
|
|
43
32
|
- id: improvement
|
|
44
33
|
name: Improvement
|
|
45
34
|
description: "기존 기능 개선 템플릿"
|
|
46
|
-
notion_template: improvement
|
|
47
35
|
linear_labels: [dev]
|
|
48
36
|
linear_priority: p2
|
|
49
37
|
- id: refactor
|
|
50
38
|
name: Refactor
|
|
51
39
|
description: "리팩토링 템플릿"
|
|
52
|
-
notion_template: refactor
|
|
53
40
|
linear_labels: [refactor]
|
|
54
41
|
linear_priority: p3
|
|
55
42
|
- id: design
|
|
56
|
-
name: Design
|
|
43
|
+
name: Design
|
|
57
44
|
description: "설계 문서 템플릿"
|
|
58
|
-
notion_template: design-doc
|
|
59
45
|
linear_labels: [design, plan]
|
|
60
46
|
linear_priority: p2
|
|
61
47
|
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -26,8 +26,8 @@ function validateTemplates(templates, labelIds) {
|
|
|
26
26
|
throw new ConfigValidationError("templates array is empty or missing.");
|
|
27
27
|
}
|
|
28
28
|
for (const tmpl of templates) {
|
|
29
|
-
if (!tmpl.id || !tmpl.name || !tmpl.description
|
|
30
|
-
throw new ConfigValidationError(`Template '${tmpl.id ?? "(unknown)"}' is missing required fields (id, name, description
|
|
29
|
+
if (!tmpl.id || !tmpl.name || !tmpl.description) {
|
|
30
|
+
throw new ConfigValidationError(`Template '${tmpl.id ?? "(unknown)"}' is missing required fields (id, name, description).`);
|
|
31
31
|
}
|
|
32
32
|
if (Array.isArray(tmpl.linear_labels)) {
|
|
33
33
|
for (const lid of tmpl.linear_labels) {
|
package/dist/env.js
CHANGED
|
@@ -20,7 +20,6 @@ export function resolveFile(filename) {
|
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
22
|
const REQUIRED_KEYS = ["LINEAR_API_KEY", "LINEAR_DEFAULT_TEAM_ID"];
|
|
23
|
-
const NOTION_KEYS = ["NOTION_API_KEY", "NOTION_ROOT_PAGE_ID"];
|
|
24
23
|
const KEY_HELP = {
|
|
25
24
|
LINEAR_API_KEY: "Linear > Settings > API > Personal API Keys",
|
|
26
25
|
LINEAR_DEFAULT_TEAM_ID: "'npx pm-skill init' to discover your team ID",
|
|
@@ -66,13 +65,6 @@ export function validateEnv(command) {
|
|
|
66
65
|
missing.push(key);
|
|
67
66
|
}
|
|
68
67
|
}
|
|
69
|
-
const notionCommands = ["start-feature", "report-bug"];
|
|
70
|
-
if (notionCommands.includes(command)) {
|
|
71
|
-
for (const key of NOTION_KEYS) {
|
|
72
|
-
if (!process.env[key])
|
|
73
|
-
missing.push(key);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
68
|
if (missing.length > 0) {
|
|
77
69
|
const hints = missing
|
|
78
70
|
.map((k) => ` ${k}: ${KEY_HELP[k] ?? ""}`)
|
package/dist/notion.d.ts
CHANGED
|
@@ -9,24 +9,6 @@ export declare function validateNotionKey(apiKey: string): Promise<{
|
|
|
9
9
|
id: string;
|
|
10
10
|
name: string;
|
|
11
11
|
}>;
|
|
12
|
-
export declare function buildFeaturePRD(title: string, linearUrl: string): BlockObjectRequest[];
|
|
13
|
-
export declare function buildBugReport(title: string, severity: string, linearUrl: string): BlockObjectRequest[];
|
|
14
|
-
export declare function buildDesignDoc(title: string, linearUrl: string): BlockObjectRequest[];
|
|
15
|
-
export declare function buildImprovement(title: string, linearUrl: string): BlockObjectRequest[];
|
|
16
|
-
export declare function buildRefactor(title: string, linearUrl: string): BlockObjectRequest[];
|
|
17
|
-
export declare function getTemplateBlocks(notionTemplate: string, title: string, linearUrl: string, severity?: string): BlockObjectRequest[];
|
|
18
|
-
export declare function createPage(client: Client, parentPageId: string, title: string, blocks: BlockObjectRequest[]): Promise<{
|
|
19
|
-
id: string;
|
|
20
|
-
url: string;
|
|
21
|
-
}>;
|
|
22
|
-
export declare function createDatabaseEntry(client: Client, databaseId: string, properties: Record<string, unknown>): Promise<{
|
|
23
|
-
id: string;
|
|
24
|
-
url: string;
|
|
25
|
-
}>;
|
|
26
|
-
export declare function createTemplatedPage(client: Client, parentPageId: string, notionTemplate: string, title: string, linearUrl: string, severity?: string): Promise<{
|
|
27
|
-
id: string;
|
|
28
|
-
url: string;
|
|
29
|
-
}>;
|
|
30
12
|
export declare function getPage(client: Client, pageId: string): Promise<Record<string, unknown>>;
|
|
31
13
|
export declare function searchPages(client: Client, query: string): Promise<Array<{
|
|
32
14
|
id: string;
|
package/dist/notion.js
CHANGED
|
@@ -22,193 +22,7 @@ export async function validateNotionKey(apiKey) {
|
|
|
22
22
|
throw new Error("Notion API key validation failed. Check your key at: https://www.notion.so/my-integrations");
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
// ── Block Builders ──
|
|
26
|
-
function text(content) {
|
|
27
|
-
return [{ type: "text", text: { content } }];
|
|
28
|
-
}
|
|
29
|
-
function heading1(content) {
|
|
30
|
-
return {
|
|
31
|
-
object: "block",
|
|
32
|
-
type: "heading_1",
|
|
33
|
-
heading_1: { rich_text: text(content) },
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function heading2(content) {
|
|
37
|
-
return {
|
|
38
|
-
object: "block",
|
|
39
|
-
type: "heading_2",
|
|
40
|
-
heading_2: { rich_text: text(content) },
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function paragraph(content) {
|
|
44
|
-
return {
|
|
45
|
-
object: "block",
|
|
46
|
-
type: "paragraph",
|
|
47
|
-
paragraph: { rich_text: text(content) },
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function todo(content, checked = false) {
|
|
51
|
-
return {
|
|
52
|
-
object: "block",
|
|
53
|
-
type: "to_do",
|
|
54
|
-
to_do: { rich_text: text(content), checked },
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function numberedItem(content) {
|
|
58
|
-
return {
|
|
59
|
-
object: "block",
|
|
60
|
-
type: "numbered_list_item",
|
|
61
|
-
numbered_list_item: { rich_text: text(content) },
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function bookmark(url) {
|
|
65
|
-
return {
|
|
66
|
-
object: "block",
|
|
67
|
-
type: "bookmark",
|
|
68
|
-
bookmark: { url },
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function divider() {
|
|
72
|
-
return {
|
|
73
|
-
object: "block",
|
|
74
|
-
type: "divider",
|
|
75
|
-
divider: {},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
// ── Template Builders ──
|
|
79
|
-
export function buildFeaturePRD(title, linearUrl) {
|
|
80
|
-
return [
|
|
81
|
-
heading2(`📋 ${title}`),
|
|
82
|
-
bookmark(linearUrl),
|
|
83
|
-
divider(),
|
|
84
|
-
heading1("목표"),
|
|
85
|
-
paragraph(""),
|
|
86
|
-
heading1("배경"),
|
|
87
|
-
paragraph(""),
|
|
88
|
-
heading1("요구사항"),
|
|
89
|
-
todo("요구사항 1"),
|
|
90
|
-
todo("요구사항 2"),
|
|
91
|
-
todo("요구사항 3"),
|
|
92
|
-
heading1("설계"),
|
|
93
|
-
paragraph(""),
|
|
94
|
-
heading1("테스트 계획"),
|
|
95
|
-
paragraph(""),
|
|
96
|
-
];
|
|
97
|
-
}
|
|
98
|
-
export function buildBugReport(title, severity, linearUrl) {
|
|
99
|
-
return [
|
|
100
|
-
heading2(`🐛 ${title} (${severity})`),
|
|
101
|
-
bookmark(linearUrl),
|
|
102
|
-
divider(),
|
|
103
|
-
heading1("재현 단계"),
|
|
104
|
-
numberedItem("단계 1"),
|
|
105
|
-
numberedItem("단계 2"),
|
|
106
|
-
numberedItem("단계 3"),
|
|
107
|
-
heading1("예상 동작"),
|
|
108
|
-
paragraph(""),
|
|
109
|
-
heading1("실제 동작"),
|
|
110
|
-
paragraph(""),
|
|
111
|
-
heading1("환경"),
|
|
112
|
-
paragraph("OS / 브라우저 / 디바이스"),
|
|
113
|
-
heading1("해결 방안"),
|
|
114
|
-
paragraph(""),
|
|
115
|
-
];
|
|
116
|
-
}
|
|
117
|
-
export function buildDesignDoc(title, linearUrl) {
|
|
118
|
-
return [
|
|
119
|
-
heading2(`📐 ${title}`),
|
|
120
|
-
bookmark(linearUrl),
|
|
121
|
-
divider(),
|
|
122
|
-
heading1("개요"),
|
|
123
|
-
paragraph(""),
|
|
124
|
-
heading1("제약 조건"),
|
|
125
|
-
paragraph(""),
|
|
126
|
-
heading1("옵션 비교"),
|
|
127
|
-
paragraph(""),
|
|
128
|
-
heading1("결정"),
|
|
129
|
-
paragraph(""),
|
|
130
|
-
heading1("후속 작업"),
|
|
131
|
-
todo("후속 작업 1"),
|
|
132
|
-
todo("후속 작업 2"),
|
|
133
|
-
];
|
|
134
|
-
}
|
|
135
|
-
export function buildImprovement(title, linearUrl) {
|
|
136
|
-
return [
|
|
137
|
-
heading2(`✨ ${title}`),
|
|
138
|
-
bookmark(linearUrl),
|
|
139
|
-
divider(),
|
|
140
|
-
heading1("현재 상태"),
|
|
141
|
-
paragraph(""),
|
|
142
|
-
heading1("개선 목표"),
|
|
143
|
-
paragraph(""),
|
|
144
|
-
heading1("변경 사항"),
|
|
145
|
-
todo("변경 1"),
|
|
146
|
-
todo("변경 2"),
|
|
147
|
-
todo("변경 3"),
|
|
148
|
-
heading1("영향 범위"),
|
|
149
|
-
paragraph(""),
|
|
150
|
-
];
|
|
151
|
-
}
|
|
152
|
-
export function buildRefactor(title, linearUrl) {
|
|
153
|
-
return [
|
|
154
|
-
heading2(`🔧 ${title}`),
|
|
155
|
-
bookmark(linearUrl),
|
|
156
|
-
divider(),
|
|
157
|
-
heading1("리팩토링 대상"),
|
|
158
|
-
paragraph(""),
|
|
159
|
-
heading1("현재 문제점"),
|
|
160
|
-
paragraph(""),
|
|
161
|
-
heading1("변경 계획"),
|
|
162
|
-
todo("단계 1"),
|
|
163
|
-
todo("단계 2"),
|
|
164
|
-
heading1("검증 방법"),
|
|
165
|
-
paragraph(""),
|
|
166
|
-
];
|
|
167
|
-
}
|
|
168
|
-
// ── Template Dispatcher ──
|
|
169
|
-
const TEMPLATE_BUILDERS = {
|
|
170
|
-
"feature-prd": buildFeaturePRD,
|
|
171
|
-
"bug-report": (t, u, s) => buildBugReport(t, s ?? "medium", u),
|
|
172
|
-
"design-doc": buildDesignDoc,
|
|
173
|
-
improvement: buildImprovement,
|
|
174
|
-
refactor: buildRefactor,
|
|
175
|
-
};
|
|
176
|
-
export function getTemplateBlocks(notionTemplate, title, linearUrl, severity) {
|
|
177
|
-
const builder = TEMPLATE_BUILDERS[notionTemplate];
|
|
178
|
-
if (!builder) {
|
|
179
|
-
const available = Object.keys(TEMPLATE_BUILDERS).join(", ");
|
|
180
|
-
throw new Error(`notion_template '${notionTemplate}'에 대한 빌더가 없습니다.\n등록된 템플릿: ${available}`);
|
|
181
|
-
}
|
|
182
|
-
return builder(title, linearUrl, severity);
|
|
183
|
-
}
|
|
184
25
|
// ── Page CRUD ──
|
|
185
|
-
export async function createPage(client, parentPageId, title, blocks) {
|
|
186
|
-
const response = await client.pages.create({
|
|
187
|
-
parent: { page_id: parentPageId },
|
|
188
|
-
properties: {
|
|
189
|
-
title: { title: [{ text: { content: title } }] },
|
|
190
|
-
},
|
|
191
|
-
children: blocks,
|
|
192
|
-
});
|
|
193
|
-
return {
|
|
194
|
-
id: response.id,
|
|
195
|
-
url: `https://notion.so/${response.id.replace(/-/g, "")}`,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
export async function createDatabaseEntry(client, databaseId, properties) {
|
|
199
|
-
const response = await client.pages.create({
|
|
200
|
-
parent: { database_id: databaseId },
|
|
201
|
-
properties: properties,
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
id: response.id,
|
|
205
|
-
url: `https://notion.so/${response.id.replace(/-/g, "")}`,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
export async function createTemplatedPage(client, parentPageId, notionTemplate, title, linearUrl, severity) {
|
|
209
|
-
const blocks = getTemplateBlocks(notionTemplate, title, linearUrl, severity);
|
|
210
|
-
return createPage(client, parentPageId, title, blocks);
|
|
211
|
-
}
|
|
212
26
|
export async function getPage(client, pageId) {
|
|
213
27
|
return (await client.pages.retrieve({ page_id: pageId }));
|
|
214
28
|
}
|
package/dist/workflows.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolve, dirname } from "path";
|
|
|
5
5
|
import { validateEnv, writeEnvFile, PKG_ROOT } from "./env.js";
|
|
6
6
|
import { loadConfig, getTemplate, resolvePriority, resolveSeverity, validateDocType, validateLabel, } from "./config.js";
|
|
7
7
|
import { getLinearClient, validateLinearKey, createIssue, deleteIssue, getIssue, getIssueDetail, createRelation, createAttachment, createLabel, getTeams, getTeamStates, getTeamLabels, getTeamProjects, resolveLabels, } from "./linear.js";
|
|
8
|
-
import { getNotionClient,
|
|
8
|
+
import { getNotionClient, validateNotionKey, searchPages, createPageFromMarkdown, updatePageContent, deletePage, extractNotionPageId, } from "./notion.js";
|
|
9
9
|
// ── Init ──
|
|
10
10
|
function copyBundledFile(srcName, destPath) {
|
|
11
11
|
if (existsSync(destPath)) {
|
|
@@ -243,24 +243,22 @@ async function startFeature(ctx, args) {
|
|
|
243
243
|
const priority = tmpl.linear_priority
|
|
244
244
|
? resolvePriority(ctx.config, tmpl.linear_priority)
|
|
245
245
|
: undefined;
|
|
246
|
+
const description = [
|
|
247
|
+
`## Tasks`,
|
|
248
|
+
`- [ ] Implementation`,
|
|
249
|
+
`- [ ] Write/update documentation (\`push-doc\`)`,
|
|
250
|
+
`- [ ] Tests`,
|
|
251
|
+
`- [ ] Review`,
|
|
252
|
+
].join("\n");
|
|
246
253
|
const issue = await createIssue(ctx.linear, {
|
|
247
254
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
248
255
|
title,
|
|
256
|
+
description,
|
|
249
257
|
priority,
|
|
250
258
|
labelIds,
|
|
251
259
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
252
260
|
});
|
|
253
|
-
console.log(
|
|
254
|
-
if (!ctx.notion || !ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
255
|
-
console.log("[Notion] Not configured — skipping page creation");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url);
|
|
259
|
-
console.log(`[Notion] Page created: ${page.url}`);
|
|
260
|
-
await createAttachment(ctx.linear, issue.id, page.url, `${title} — PRD`);
|
|
261
|
-
console.log(`[Link] Linear ↔ Notion linked`);
|
|
262
|
-
console.log(`\n✅ Feature started: ${issue.identifier} | Notion: ${page.url}`);
|
|
263
|
-
console.log(` Notion Page ID: ${page.id} (use with --parent for sub-docs)`);
|
|
261
|
+
console.log(`✅ Feature started: ${issue.identifier} — ${issue.url}`);
|
|
264
262
|
}
|
|
265
263
|
async function reportBug(ctx, args) {
|
|
266
264
|
const title = args._[0];
|
|
@@ -273,38 +271,25 @@ async function reportBug(ctx, args) {
|
|
|
273
271
|
const teamLabels = await getTeamLabels(ctx.linear, ctx.env.LINEAR_DEFAULT_TEAM_ID);
|
|
274
272
|
const configLabels = tmpl.linear_labels.map((lid) => validateLabel(ctx.config, lid).name);
|
|
275
273
|
const labelIds = resolveLabels(configLabels, teamLabels);
|
|
274
|
+
const description = [
|
|
275
|
+
`**Severity: ${severity}**`,
|
|
276
|
+
``,
|
|
277
|
+
`## Tasks`,
|
|
278
|
+
`- [ ] Reproduce`,
|
|
279
|
+
`- [ ] Root cause analysis`,
|
|
280
|
+
`- [ ] Fix & write tests`,
|
|
281
|
+
`- [ ] Write/update documentation (\`push-doc\`)`,
|
|
282
|
+
`- [ ] Review`,
|
|
283
|
+
].join("\n");
|
|
276
284
|
const issue = await createIssue(ctx.linear, {
|
|
277
285
|
teamId: ctx.env.LINEAR_DEFAULT_TEAM_ID,
|
|
278
286
|
title,
|
|
287
|
+
description,
|
|
279
288
|
priority,
|
|
280
289
|
labelIds,
|
|
281
290
|
projectId: ctx.env.LINEAR_DEFAULT_PROJECT_ID,
|
|
282
291
|
});
|
|
283
|
-
console.log(
|
|
284
|
-
if (!ctx.notion) {
|
|
285
|
-
console.log("[Notion] Not configured — skipping");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
let notionUrl;
|
|
289
|
-
if (ctx.env.NOTION_BUG_DB_ID) {
|
|
290
|
-
const entry = await createDatabaseEntry(ctx.notion, ctx.env.NOTION_BUG_DB_ID, {
|
|
291
|
-
Name: { title: [{ text: { content: title } }] },
|
|
292
|
-
});
|
|
293
|
-
notionUrl = entry.url;
|
|
294
|
-
console.log(`[Notion] Bug DB entry: ${notionUrl}`);
|
|
295
|
-
}
|
|
296
|
-
else if (ctx.env.NOTION_ROOT_PAGE_ID) {
|
|
297
|
-
const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issue.url, severity);
|
|
298
|
-
notionUrl = page.url;
|
|
299
|
-
console.log(`[Notion] Bug report page: ${notionUrl}`);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
console.log("[Notion] NOTION_ROOT_PAGE_ID not set — skipping");
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
await createAttachment(ctx.linear, issue.id, notionUrl, `${title} — Bug Report`);
|
|
306
|
-
console.log(`[Link] Linear ↔ Notion linked`);
|
|
307
|
-
console.log(`\n✅ Bug reported: ${issue.identifier} | Notion: ${notionUrl}`);
|
|
292
|
+
console.log(`✅ Bug reported: ${issue.identifier} (severity: ${severity}) — ${issue.url}`);
|
|
308
293
|
}
|
|
309
294
|
async function addTask(ctx, args) {
|
|
310
295
|
const parentIdentifier = args._[0];
|
|
@@ -395,14 +380,14 @@ async function get(ctx, args) {
|
|
|
395
380
|
}
|
|
396
381
|
}
|
|
397
382
|
async function pushDoc(ctx, args) {
|
|
398
|
-
const
|
|
399
|
-
const filePath = args._[1];
|
|
383
|
+
const filePath = args._[0];
|
|
400
384
|
const content = args.content;
|
|
401
385
|
const title = args.title;
|
|
402
386
|
const parentPageId = args.parent;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
387
|
+
const issueId = args.issue;
|
|
388
|
+
if (!filePath && !content) {
|
|
389
|
+
throw new Error("Usage: npx pm-skill push-doc <file.md> [--title T] [--parent P] [--issue I]\n" +
|
|
390
|
+
" npx pm-skill push-doc --title T --content \"# md\" [--parent P] [--issue I]");
|
|
406
391
|
}
|
|
407
392
|
if (!ctx.notion) {
|
|
408
393
|
throw new Error("Notion is not configured. Run 'npx pm-skill init' with --notion-key.");
|
|
@@ -413,27 +398,31 @@ async function pushDoc(ctx, args) {
|
|
|
413
398
|
}
|
|
414
399
|
// Read markdown
|
|
415
400
|
let markdown;
|
|
416
|
-
if (filePath) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
401
|
+
if (filePath && !existsSync(filePath) && !content) {
|
|
402
|
+
throw new Error(`File not found: ${filePath}`);
|
|
403
|
+
}
|
|
404
|
+
if (filePath && existsSync(filePath)) {
|
|
420
405
|
markdown = readFileSync(filePath, "utf-8");
|
|
421
406
|
}
|
|
422
|
-
else {
|
|
407
|
+
else if (content) {
|
|
423
408
|
markdown = content;
|
|
424
409
|
}
|
|
410
|
+
else {
|
|
411
|
+
throw new Error("Provide a file path or --content.");
|
|
412
|
+
}
|
|
425
413
|
// Determine title
|
|
426
414
|
const docTitle = title ?? (filePath ? filePath.replace(/^.*[\\/]/, "").replace(/\.md$/, "") : "Untitled");
|
|
427
|
-
//
|
|
428
|
-
const issue = await getIssue(ctx.linear, identifier);
|
|
429
|
-
// Create Notion page under specified parent
|
|
415
|
+
// Create Notion page
|
|
430
416
|
const page = await createPageFromMarkdown(ctx.notion, targetParent, docTitle, markdown);
|
|
431
417
|
console.log(`[Notion] Page created: "${docTitle}" — ${page.url}`);
|
|
432
418
|
console.log(`[Notion] Page ID: ${page.id}`);
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
419
|
+
// Optionally link to Linear issue
|
|
420
|
+
if (issueId) {
|
|
421
|
+
const issue = await getIssue(ctx.linear, issueId);
|
|
422
|
+
await createAttachment(ctx.linear, issue.id, page.url, docTitle, "source-of-truth");
|
|
423
|
+
console.log(`[Link] Attached to ${issue.identifier}`);
|
|
424
|
+
}
|
|
425
|
+
console.log(`\n✅ Document pushed: ${page.url}`);
|
|
437
426
|
}
|
|
438
427
|
async function updateDoc(ctx, args) {
|
|
439
428
|
const pageId = args._[0];
|
|
@@ -488,11 +477,43 @@ async function createFolder(ctx, args) {
|
|
|
488
477
|
}
|
|
489
478
|
async function del(ctx, args) {
|
|
490
479
|
const identifiers = args._;
|
|
480
|
+
const recursive = !!args.recursive;
|
|
491
481
|
if (identifiers.length === 0) {
|
|
492
|
-
throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...]");
|
|
482
|
+
throw new Error("Usage: npx pm-skill delete <issue> [issue2 ...] [--recursive]");
|
|
493
483
|
}
|
|
494
484
|
for (const identifier of identifiers) {
|
|
495
485
|
const detail = await getIssueDetail(ctx.linear, identifier);
|
|
486
|
+
// Check for children
|
|
487
|
+
if (detail.children.length > 0 && !recursive) {
|
|
488
|
+
console.log(`⚠️ ${detail.issue.identifier} has ${detail.children.length} sub-issue(s):`);
|
|
489
|
+
for (const child of detail.children) {
|
|
490
|
+
console.log(` ${child.identifier}: ${child.title}`);
|
|
491
|
+
}
|
|
492
|
+
throw new Error(`Use --recursive to delete ${detail.issue.identifier} and its sub-issues.`);
|
|
493
|
+
}
|
|
494
|
+
// Recursively delete children first
|
|
495
|
+
if (detail.children.length > 0 && recursive) {
|
|
496
|
+
for (const child of detail.children) {
|
|
497
|
+
const childDetail = await getIssueDetail(ctx.linear, child.identifier);
|
|
498
|
+
// Delete child's Notion pages
|
|
499
|
+
if (ctx.notion) {
|
|
500
|
+
for (const att of childDetail.attachments) {
|
|
501
|
+
if (att.url.includes("notion.so")) {
|
|
502
|
+
const pageId = extractNotionPageId(att.url);
|
|
503
|
+
if (pageId) {
|
|
504
|
+
try {
|
|
505
|
+
await deletePage(ctx.notion, pageId);
|
|
506
|
+
console.log(` [Notion] Deleted: ${att.title}`);
|
|
507
|
+
}
|
|
508
|
+
catch { /* skip */ }
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
await deleteIssue(ctx.linear, child.id);
|
|
514
|
+
console.log(` Deleted sub-issue: ${child.identifier}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
496
517
|
// Delete linked Notion pages
|
|
497
518
|
if (ctx.notion && detail.attachments.length > 0) {
|
|
498
519
|
for (const att of detail.attachments) {
|
|
@@ -504,7 +525,7 @@ async function del(ctx, args) {
|
|
|
504
525
|
console.log(` [Notion] Deleted: ${att.title}`);
|
|
505
526
|
}
|
|
506
527
|
catch {
|
|
507
|
-
console.log(` [Notion] Could not delete: ${att.url}
|
|
528
|
+
console.log(` [Notion] Could not delete: ${att.url}`);
|
|
508
529
|
}
|
|
509
530
|
}
|
|
510
531
|
}
|
|
@@ -555,6 +576,51 @@ async function selectProject(args) {
|
|
|
555
576
|
console.log(`\nUsage: npx pm-skill select-project "<name or id>"`);
|
|
556
577
|
}
|
|
557
578
|
}
|
|
579
|
+
async function selectPage(args) {
|
|
580
|
+
const { loadEnvFile, writeEnvFile } = await import("./env.js");
|
|
581
|
+
loadEnvFile();
|
|
582
|
+
const apiKey = process.env.NOTION_API_KEY;
|
|
583
|
+
if (!apiKey) {
|
|
584
|
+
throw new Error("NOTION_API_KEY must be set. Run 'npx pm-skill init' with --notion-key first.");
|
|
585
|
+
}
|
|
586
|
+
const client = getNotionClient(apiKey);
|
|
587
|
+
const pages = await searchPages(client, "");
|
|
588
|
+
if (pages.length === 0) {
|
|
589
|
+
console.log("No pages shared with this integration.");
|
|
590
|
+
console.log("Share a page in Notion: page menu → Connections → add your integration");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const pageArg = args._[0];
|
|
594
|
+
if (pageArg) {
|
|
595
|
+
const match = pages.find((p) => p.id === pageArg || p.title.toLowerCase() === pageArg.toLowerCase());
|
|
596
|
+
if (!match) {
|
|
597
|
+
console.log("Available pages:");
|
|
598
|
+
for (const p of pages) {
|
|
599
|
+
console.log(` ${p.title} | ${p.id}`);
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Page '${pageArg}' not found.`);
|
|
602
|
+
}
|
|
603
|
+
writeEnvFile(process.cwd(), { NOTION_ROOT_PAGE_ID: match.id });
|
|
604
|
+
console.log(`✅ Selected page: "${match.title}" (${match.id})`);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
const currentId = process.env.NOTION_ROOT_PAGE_ID;
|
|
608
|
+
console.log("Available pages:");
|
|
609
|
+
for (const p of pages) {
|
|
610
|
+
const marker = p.id === currentId ? " ← current" : "";
|
|
611
|
+
console.log(` ${p.title} | ${p.id}${marker}`);
|
|
612
|
+
}
|
|
613
|
+
console.log(`\nUsage: npx pm-skill select-page "<name or id>"`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async function installCodexSkill() {
|
|
617
|
+
const { homedir } = await import("os");
|
|
618
|
+
const targetDir = resolve(homedir(), ".codex", "skills", "pm-skill");
|
|
619
|
+
const targetPath = resolve(targetDir, "SKILL.md");
|
|
620
|
+
copyBundledFile("SKILL.md", targetPath);
|
|
621
|
+
console.log(`\n✅ Codex skill installed: ${targetPath}`);
|
|
622
|
+
console.log(` Restart Codex to pick up the new skill.`);
|
|
623
|
+
}
|
|
558
624
|
// ── Command Registry ──
|
|
559
625
|
const COMMANDS = {
|
|
560
626
|
setup: (ctx, args) => setup(ctx, args),
|
|
@@ -573,8 +639,8 @@ const COMMANDS = {
|
|
|
573
639
|
// ── Main ──
|
|
574
640
|
async function main() {
|
|
575
641
|
const args = minimist(process.argv.slice(2), {
|
|
576
|
-
string: ["severity", "type", "url", "title", "content", "parent", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
577
|
-
boolean: ["sync", "version"],
|
|
642
|
+
string: ["severity", "type", "url", "title", "content", "parent", "issue", "linear-key", "notion-key", "team-id", "project-id", "notion-page"],
|
|
643
|
+
boolean: ["sync", "version", "recursive"],
|
|
578
644
|
alias: { s: "severity", t: "type" },
|
|
579
645
|
});
|
|
580
646
|
// --version
|
|
@@ -595,7 +661,9 @@ Commands:
|
|
|
595
661
|
Initialize project (validates keys, creates .env, config.yml, SKILL.md, AGENTS.md)
|
|
596
662
|
setup [--sync] Verify config & label matching (--sync creates missing labels)
|
|
597
663
|
select-project [name-or-id] List or switch Linear project
|
|
598
|
-
|
|
664
|
+
select-page [name-or-id] List or switch Notion root page
|
|
665
|
+
install-codex-skill Install skill to ~/.codex/skills/ for Codex
|
|
666
|
+
start-feature <title> Start feature (Linear issue with task checklist)
|
|
599
667
|
report-bug <title> [--severity S] File bug report (severity: urgent/high/medium/low)
|
|
600
668
|
add-task <parent> <title> Add sub-task to an issue
|
|
601
669
|
relate <issue1> <issue2> [--type T] Link issues (type: related/similar)
|
|
@@ -603,15 +671,16 @@ Commands:
|
|
|
603
671
|
attach-doc <issue> --url U --title T --type Y
|
|
604
672
|
Attach document (type: source-of-truth/issue-tracking/domain-knowledge)
|
|
605
673
|
get <issue> Show issue details
|
|
606
|
-
push-doc <
|
|
607
|
-
Upload markdown to Notion
|
|
608
|
-
push-doc
|
|
674
|
+
push-doc <file.md> [--title T] [--parent P] [--issue I]
|
|
675
|
+
Upload markdown to Notion (optionally link to issue)
|
|
676
|
+
push-doc --title T --content "# md" [--parent P] [--issue I]
|
|
609
677
|
Push content directly (for AI agents)
|
|
610
678
|
update-doc <page-id> <file.md> Replace Notion page content with markdown
|
|
611
679
|
update-doc <page-id> --content "# md"
|
|
612
680
|
Replace content directly
|
|
613
681
|
create-folder <name> [--parent P] Create Notion folder (returns page ID for --parent)
|
|
614
|
-
delete <issue> [issue2 ...]
|
|
682
|
+
delete <issue> [issue2 ...] [--recursive]
|
|
683
|
+
Delete issue(s) + linked Notion pages (--recursive for sub-issues)
|
|
615
684
|
help Show this help
|
|
616
685
|
--version Show version
|
|
617
686
|
|
|
@@ -627,6 +696,14 @@ All config is per-project (CWD). Run 'npx pm-skill init' in each project.`);
|
|
|
627
696
|
await selectProject(args);
|
|
628
697
|
return;
|
|
629
698
|
}
|
|
699
|
+
if (command === "select-page") {
|
|
700
|
+
await selectPage(args);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (command === "install-codex-skill") {
|
|
704
|
+
await installCodexSkill();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
630
707
|
const cmdFn = COMMANDS[command];
|
|
631
708
|
if (!cmdFn) {
|
|
632
709
|
const available = ["init", ...Object.keys(COMMANDS)].join(", ");
|