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 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
- # If installed globally via npm:
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
- - **`.env`** API keys and IDs. Looked up in: CWD → `~/.pm-skill/` → package root.
74
- - **`config.yml`** — Labels, templates, priorities, severity mappings. Same lookup order.
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
- ## Installation
18
-
19
- ### Option A: npm (recommended)
18
+ ## Quick Start
20
19
 
21
20
  ```bash
22
- npm install -g pm-skill
23
- pm-skill help
24
- ```
25
-
26
- ### Option B: npx (no install)
21
+ cd your-project
27
22
 
28
- ```bash
29
- npx pm-skill help
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
- ### Option C: Clone as Claude Code skill
26
+ # Verify label matching
27
+ npx pm-skill setup
33
28
 
34
- ```bash
35
- git clone https://github.com/Leonamin/pm-skill.git ~/.claude/skills/pm-skill
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
- ### Option D: Clone for Codex
33
+ This creates the following in your project:
40
34
 
41
- ```bash
42
- git clone https://github.com/Leonamin/pm-skill.git ~/pm-skill
43
- cd ~/pm-skill && npm install
44
- # Codex will read AGENTS.md for instructions
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
- ```bash
57
- # Copy the template
58
- cp .env.example ~/.pm-skill/.env
59
- # Edit with your API keys
60
- ```
46
+ ### API Keys
61
47
 
62
- ```env
63
- LINEAR_API_KEY=lin_api_xxxxxxxx
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
- ### 2. Run setup
51
+ ### init options
70
52
 
71
53
  ```bash
72
- pm-skill setup
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
- This shows your Linear teams, workflow states, and labels — and verifies that `config.yml` labels match your Linear workspace.
61
+ ### Customize `config.yml`
76
62
 
77
- ### 3. Customize `config.yml`
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
- If installed as a skill in `~/.claude/skills/pm-skill/`, Claude Code auto-discovers it via `SKILL.md`. You can invoke commands through natural language:
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
- Clone the repo and Codex reads `AGENTS.md` for command instructions. Run commands via:
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 Notion 정형화된 안에서 조작하는 프로젝트 관리 스킬.
4
- "설계의 자유, 사용의 부자유" — config.yml 정의된 라벨/템플릿/severity만 사용 가능.
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
- npx tsx src/workflows.ts setup
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
- ### 4. Config 커스터마이징
12
+ # Verify label matching
13
+ npx pm-skill setup
29
14
 
30
- `config.yml`에서 라벨, 템플릿, severity 매핑을 프로젝트에 맞게 수정.
31
- **규칙: 모든 라벨/템플릿에 `description` 필수.** 없으면 로딩 시 에러.
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 연결 상태 확인 + .env 안내.
21
+ ### setup [--sync]
22
+ Verify Linear/Notion connection + label matching. `--sync` creates missing labels.
44
23
  ```bash
45
- npx tsx src/workflows.ts setup
24
+ npx pm-skill setup
46
25
  ```
47
26
 
48
27
  ### start-feature
49
- 기능 개발 시작. Linear 이슈 + Notion PRD + 상호 URL 연결.
28
+ Start feature development. Creates Linear issue + Notion PRD + bidirectional links.
50
29
  ```bash
51
- npx tsx src/workflows.ts start-feature "예약 취소 기능"
30
+ npx pm-skill start-feature "Feature title"
52
31
  ```
53
32
 
54
33
  ### report-bug
55
- 버그 리포트. severity로 우선순위 자동 매핑.
34
+ File bug report. Severity maps to Linear priority.
56
35
  ```bash
57
- npx tsx src/workflows.ts report-bug "결제 금액 오류" --severity high
58
- # severity: urgent, high, medium(기본), low
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 tsx src/workflows.ts add-task ENG-10 "단위 테스트 작성"
43
+ npx pm-skill add-task ENG-10 "Write unit tests"
65
44
  ```
66
45
 
67
46
  ### relate
68
- 이슈 관계 설정. (related, similar)
47
+ Link two issues. (related, similar)
69
48
  ```bash
70
- npx tsx src/workflows.ts relate ENG-10 ENG-11 --type related
49
+ npx pm-skill relate ENG-10 ENG-11 --type related
71
50
  ```
72
51
 
73
52
  ### block
74
- 선행 관계 설정. (ENG-10 완료 ENG-11 진행)
53
+ Set blocking relationship. (ENG-10 must complete before ENG-11)
75
54
  ```bash
76
- npx tsx src/workflows.ts block ENG-10 ENG-11
55
+ npx pm-skill block ENG-10 ENG-11
77
56
  ```
78
57
 
79
58
  ### attach-doc
80
- 이슈에 문서 URL 첨부. doc_type은 config 검증.
59
+ Attach document URL to issue. Type is validated against config.
81
60
  ```bash
82
- npx tsx src/workflows.ts attach-doc ENG-10 \
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 tsx src/workflows.ts get ENG-10
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
- # 1. 기능 시작
100
- npx tsx src/workflows.ts start-feature "예약 취소 기능"
101
- # Linear ENG-10 + Notion PRD 생성
102
-
103
- # 2. 하위 태스크 추가
104
- npx tsx src/workflows.ts add-task ENG-10 "API 엔드포인트 구현"
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
- # 1. 버그 리포트
118
- npx tsx src/workflows.ts report-bug "결제 금액 오류" --severity high
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` | 사용 가능한 라벨 목록 (description 필수) |
130
- | `templates` | 커맨드별 기본 라벨/우선순위/Notion 템플릿 매핑 |
131
- | `priorities` | Plank 우선순위 → Linear 우선순위 매핑 (p0-p3) |
132
- | `severity_mapping` | severity 이름 → priority 매핑 |
133
- | `doc_types` | 문서 유형 (attach-doc --type 값) |
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 GLOBAL_DIR: string;
1
+ export declare const PKG_ROOT: string;
2
2
  /**
3
- * Resolve a file by checking multiple directories in order:
4
- * 1. CWD (project-local override)
5
- * 2. ~/.pm-skill/ (user-global config)
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
- * Hierarchical .env loading:
19
- * 1. CWD/.env (project-specific, highest priority)
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 loadEnvFiles(): void;
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 multiple directories in order:
10
- * 1. CWD (project-local override)
11
- * 2. ~/.pm-skill/ (user-global config)
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' or 'pm-skill setup' to discover your team ID",
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 a single .env file and set values into process.env.
38
- * Does NOT overwrite existing values (earlier loads take precedence).
33
+ * Parse CWD/.env and set values into process.env.
34
+ * Does NOT overwrite existing env vars.
39
35
  */
40
- function parseEnvFile(filePath) {
41
- const content = readFileSync(filePath, "utf-8");
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
- loadEnvFiles();
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' to set up, or create .env manually.\n` +
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 { validateEnv, writeEnvFile, GLOBAL_DIR } from "./env.js";
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 (runs before context — no env/config validation needed) ──
8
- import { existsSync, copyFileSync, mkdirSync } from "fs";
9
- import { resolve, dirname } from "path";
10
- import { fileURLToPath } from "url";
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, "config.yml");
15
+ const src = resolve(PKG_ROOT, srcName);
20
16
  if (!existsSync(src)) {
21
- console.log(" Warning: bundled config.yml not found — skipping copy");
17
+ console.log(` Warning: bundled ${srcName} not found — skipping`);
22
18
  return;
23
19
  }
24
- mkdirSync(targetDir, { recursive: true });
25
- copyFileSync(src, targetConfig);
26
- console.log(` config.yml copied to ${targetConfig}`);
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>] [--global]\n" +
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 targetDir = isGlobal ? GLOBAL_DIR : process.cwd();
45
- const targetLabel = isGlobal ? `~/.pm-skill/` : "CWD";
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(`\n[Config] Writing .env to ${targetLabel}...`);
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(targetDir, envEntries);
90
+ const envPath = writeEnvFile(cwd, envEntries);
98
91
  console.log(` Written: ${envPath}`);
99
- // 5. Copy config.yml if missing
100
- console.log(`\n[Config] Checking config.yml...`);
101
- copyDefaultConfig(targetDir);
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: ${envPath}`);
105
- console.log(` config.yml: ${resolve(targetDir, "config.yml")}`);
106
- console.log(` Linear user: ${linearUser.name}`);
107
- console.log(` Team: ${selectedTeamId}`);
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: connected`);
105
+ console.log(` Notion: connected`);
110
106
  console.log(`\nNext steps:`);
111
- if (isGlobal) {
112
- console.log(` - Per-project overrides: create .env in your project directory`);
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
- let teamLabels = await getTeamLabels(ctx.linear, teamId);
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. .env guide
174
- console.log("\n📝 .env reference:");
175
- console.log(` LINEAR_API_KEY=<your key>`);
176
- console.log(` LINEAR_DEFAULT_TEAM_ID=${teamId}`);
177
- if (ctx.env.LINEAR_DEFAULT_PROJECT_ID) {
178
- console.log(` LINEAR_DEFAULT_PROJECT_ID=${ctx.env.LINEAR_DEFAULT_PROJECT_ID}`);
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(" LINEAR_DEFAULT_PROJECT_ID=<optional>");
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("사용법: start-feature <제목>");
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
- const issueId = issue.identifier;
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] Notion 설정 없음 페이지 생성 생략");
213
+ console.log("[Notion] Not configuredskipping page creation");
213
214
  return;
214
215
  }
215
- const page = await createTemplatedPage(ctx.notion, ctx.env.NOTION_ROOT_PAGE_ID, tmpl.notion_template, title, issueUrl);
216
- console.log(`[Notion] 페이지 생성: ${page.url}`);
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✅ 기능 시작: ${issueId} | Notion: ${page.url}`);
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("사용법: report-bug <제목> [--severity high]");
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
- const issueId = issue.identifier;
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] Notion 설정 없음 문서 생성 생략");
242
+ console.log("[Notion] Not configuredskipping");
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] 버그 DB 엔트리 생성: ${notionUrl}`);
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] 버그리포트 페이지 생성: ${notionUrl}`);
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✅ 버그 리포트: ${issueId} | Notion: ${notionUrl}`);
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("사용법: add-task <부모이슈> <제목>");
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(`✅ 하위 이슈 생성: ${child.identifier} (부모: ${parent.identifier})`);
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("사용법: relate <이슈1> <이슈2> [--type related]");
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 커맨드는 'related' 또는 'similar' 타입만 지원합니다. blocks 관계는 'block' 커맨드를 사용하세요.`);
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("사용법: block <선행이슈> <후행이슈>");
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} ${id2}를 선행합니다 (blocks)`);
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('사용법: attach-doc <이슈> --url "URL" --title "제목" --type <유형>');
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} 문서 첨부: "${title}" (${type}) — ${url}`);
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("사용법: get <이슈>");
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(`상태: ${state?.name ?? "?"} | 우선순위: ${issue.priority ?? "?"} | 라벨: ${labelNames || "없음"}`);
331
+ console.log(`State: ${state?.name ?? "?"} | Priority: ${issue.priority ?? "?"} | Labels: ${labelNames || "none"}`);
339
332
  if (children.length > 0) {
340
- console.log("\n하위 이슈:");
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("\n관계:");
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("\n첨부 문서:");
347
+ console.log("\nAttachments:");
355
348
  for (const att of attachments) {
356
349
  const typeLabel = att.subtitle ? ` (${att.subtitle})` : "";
357
- console.log(` 📄 ${att.title}${typeLabel} — ${att.url}`);
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: ["global", "sync"],
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); // command 제거, 나머지가 positional args
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] [--global]
388
- Initialize config & validate API keys
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
- Config lookup: CWD/.env + ~/.pm-skill/.env (both loaded, CWD wins)`);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pm-skill",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Structured project management CLI — Linear + Notion integration for AI coding assistants (Claude Code, Codex)",
5
5
  "type": "module",
6
6
  "bin": {