@warnyin/agents 0.13.0 → 0.15.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/CHANGELOG.md +139 -125
- package/README.md +160 -148
- package/package.json +38 -38
- package/src/.claude/agents/warnyin-infra.md +13 -13
- package/src/.claude/agents/warnyin-qa.md +13 -13
- package/src/.claude/agents/warnyin-sa.md +13 -13
- package/src/.claude/agents/warnyin-security.md +13 -13
- package/src/.claude/agents/warnyin-tech-lead.md +13 -13
- package/src/.claude/commands/warnyin/build.md +31 -31
- package/src/.claude/commands/warnyin/design.md +27 -27
- package/src/.claude/commands/warnyin/discovery.md +22 -17
- package/src/.claude/commands/warnyin/explore.md +14 -14
- package/src/.claude/commands/warnyin/init.md +12 -12
- package/src/.claude/commands/warnyin/install-skill.md +14 -14
- package/src/.claude/commands/warnyin/next.md +17 -17
- package/src/.claude/commands/warnyin/ship.md +28 -28
- package/src/.claude/commands/warnyin/triage.md +14 -14
- package/src/.claude/commands/warnyin/update-codemaps.md +12 -12
- package/src/.claude/commands/warnyin/verify.md +20 -20
- package/src/.claude/skills/explore/SKILL.md +8 -8
- package/src/.claude/skills/next/SKILL.md +8 -8
- package/src/.claude/skills/update-codemaps/SKILL.md +8 -8
- package/src/.warnyin/installer/templates/CLAUDE.global.md +5 -5
- package/src/.warnyin/installer/templates/CLAUDE.md +34 -34
- package/src/.warnyin/template/docs/codemap/index.md +18 -18
- package/src/.warnyin/template/docs/features/[feature-name]/business.md +5 -5
- package/src/.warnyin/template/docs/features/[feature-name]/feature.md +5 -5
- package/src/.warnyin/template/docs/features/[feature-name]/spec.md +16 -16
- package/src/.warnyin/template/docs/infra.md +16 -16
- package/src/.warnyin/template/docs/project.md +18 -18
- package/src/.warnyin/template/docs/rule.md +7 -7
- package/src/.warnyin/template/docs/techstack/[component]/about.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/rule.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/standard.md +6 -6
- package/src/.warnyin/template/docs/techstack/[component]/structure.md +7 -7
- package/src/.warnyin/template/docs/techstack/[component]/test.md +7 -7
- package/src/.warnyin/template/docs/troubleshooting.md +32 -32
- package/src/.warnyin/template/stages/[topic]/build.md +58 -58
- package/src/.warnyin/template/stages/[topic]/business.md +21 -21
- package/src/.warnyin/template/stages/[topic]/design.md +63 -63
- package/src/.warnyin/template/stages/[topic]/discovery.md +69 -69
- package/src/.warnyin/template/stages/[topic]/proposal.md +43 -43
- package/src/.warnyin/template/stages/[topic]/research.md +49 -49
- package/src/.warnyin/template/stages/[topic]/ship.md +32 -32
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/issue.md +19 -19
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/rule.md +13 -13
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/spec.md +36 -36
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/standard.md +21 -21
- package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/task.md +40 -40
- package/src/.warnyin/template/stages/[topic]/test.md +46 -46
- package/src/.warnyin/template/stages/[topic]/troubleshooting.md +34 -34
- package/src/.warnyin/template/stages/[topic]/verify.md +44 -44
- package/src/.warnyin/workflow/README.md +105 -102
- package/src/.warnyin/workflow/api-doc.md +93 -93
- package/src/.warnyin/workflow/codemap.md +91 -91
- package/src/.warnyin/workflow/contexts/README.md +51 -51
- package/src/.warnyin/workflow/contexts/build.md +25 -25
- package/src/.warnyin/workflow/contexts/research.md +25 -25
- package/src/.warnyin/workflow/contexts/review.md +25 -25
- package/src/.warnyin/workflow/explore.md +32 -32
- package/src/.warnyin/workflow/init.md +136 -136
- package/src/.warnyin/workflow/next.md +48 -48
- package/src/.warnyin/workflow/roles/README.md +47 -47
- package/src/.warnyin/workflow/roles/ba.md +25 -25
- package/src/.warnyin/workflow/roles/developer.md +31 -31
- package/src/.warnyin/workflow/roles/infra.md +24 -24
- package/src/.warnyin/workflow/roles/po.md +28 -28
- package/src/.warnyin/workflow/roles/qa.md +35 -35
- package/src/.warnyin/workflow/roles/sa.md +28 -28
- package/src/.warnyin/workflow/roles/security.md +39 -39
- package/src/.warnyin/workflow/roles/tech-lead.md +28 -28
- package/src/.warnyin/workflow/scripts/build-wave.mjs +145 -143
- package/src/.warnyin/workflow/scripts/validate-topic.mjs +378 -378
- package/src/.warnyin/workflow/stages/build.md +98 -98
- package/src/.warnyin/workflow/stages/design.md +154 -131
- package/src/.warnyin/workflow/stages/discovery.md +256 -78
- package/src/.warnyin/workflow/stages/ship.md +94 -94
- package/src/.warnyin/workflow/stages/verify.md +82 -82
- package/src/.warnyin/workflow/triage.md +74 -74
- package/src/AGENTS.md +54 -54
- package/src/bin/cli.mjs +310 -310
package/src/bin/cli.mjs
CHANGED
|
@@ -1,310 +1,310 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* warnyin-agents installer
|
|
4
|
-
* ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน (cwd) หรือแบบ global (~/)
|
|
5
|
-
*
|
|
6
|
-
* npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ข้ามไฟล์ที่มีอยู่แล้ว)
|
|
7
|
-
* npx @warnyin/agents --global ติดตั้งแบบ global (~/) ใช้ได้ทุกโปรเจกต์
|
|
8
|
-
* npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ — ไม่ถาม)
|
|
9
|
-
* npx @warnyin/agents --update อัปเดต playbook กลาง (เขียนทับเฉพาะไฟล์ core)
|
|
10
|
-
* npx @warnyin/agents --dry-run แสดงว่าจะทำอะไร โดยไม่เขียนไฟล์จริง
|
|
11
|
-
* (ทางสำรองไม่ผ่าน npm: npx github:warnyin/warnyin-agents)
|
|
12
|
-
*/
|
|
13
|
-
import fs from 'node:fs'
|
|
14
|
-
import os from 'node:os'
|
|
15
|
-
import path from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
import { createInterface } from 'node:readline/promises'
|
|
18
|
-
|
|
19
|
-
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
-
const cwd = process.cwd()
|
|
21
|
-
const args = new Set(process.argv.slice(2))
|
|
22
|
-
const UPDATE = args.has('--update')
|
|
23
|
-
const DRY = args.has('--dry-run')
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* ★ pure function — เลือก mode จาก flag/TTY/answer (ไม่มี side-effect, export ให้ unit test)
|
|
27
|
-
* @param {{globalFlag?:boolean, projectFlag?:boolean, isTTY?:boolean, answer?:string}} o
|
|
28
|
-
* @returns {'project'|'global'}
|
|
29
|
-
*/
|
|
30
|
-
export function resolveMode({ globalFlag, projectFlag, isTTY, answer } = {}) {
|
|
31
|
-
if (globalFlag && projectFlag) {
|
|
32
|
-
throw new Error('--global กับ --project ใช้พร้อมกันไม่ได้ (เลือกอย่างใดอย่างหนึ่ง)')
|
|
33
|
-
}
|
|
34
|
-
if (globalFlag) return 'global'
|
|
35
|
-
if (projectFlag) return 'project'
|
|
36
|
-
if (!isTTY) return 'project' // CI-safe default — npx/pipe ไม่ค้างรอ input
|
|
37
|
-
const a = (answer ?? '').trim().toLowerCase()
|
|
38
|
-
return a === '2' || a === 'global' ? 'global' : 'project'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (args.has('--help') || args.has('-h')) {
|
|
42
|
-
console.log(`warnyin-agents — ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน หรือแบบ global
|
|
43
|
-
|
|
44
|
-
ใช้งาน:
|
|
45
|
-
npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ถ้า TTY จะถามก่อน; ข้ามไฟล์ที่มีอยู่แล้ว)
|
|
46
|
-
npx @warnyin/agents --global ติดตั้งแบบ global ลง ~/ (~/.warnyin + ~/.claude) ใช้ได้ทุกโปรเจกต์
|
|
47
|
-
npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ ไม่ถาม)
|
|
48
|
-
npx @warnyin/agents --update อัปเดต playbook กลางเป็นเวอร์ชันล่าสุด
|
|
49
|
-
(เขียนทับเฉพาะ .warnyin/workflow/, .claude/commands/warnyin/,
|
|
50
|
-
template .warnyin/template/stages/[topic] — ไม่แตะ docs/ และงานจริง)
|
|
51
|
-
npx @warnyin/agents --dry-run แสดงรายการไฟล์ที่จะสร้าง/อัปเดต โดยไม่เขียนจริง
|
|
52
|
-
|
|
53
|
-
หลังติดตั้ง: เปิด Claude Code ในโปรเจกต์ แล้วรัน /warnyin:init ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/`)
|
|
54
|
-
process.exit(0)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// guard กัน self-install — เก็บไว้แบบ defensive (zero-cost)
|
|
58
|
-
// หลังย้าย source เข้า src/ → pkgRoot = src/ (sibling ของ bin/) จึงแทบไม่มีทาง === cwd (repo root / temp sandbox)
|
|
59
|
-
// → guard นี้เป็น no-op โดยตั้งใจในเคสปกติ/sandbox; ยังคงดักได้เฉพาะ edge case ที่ install ลงโฟลเดอร์ที่เป็น src/ เอง
|
|
60
|
-
if (path.resolve(pkgRoot) === path.resolve(cwd)) {
|
|
61
|
-
console.error('✖ กำลังรันอยู่ใน repo ของ warnyin-agents เอง — ให้ cd ไปที่โปรเจกต์ปลายทางก่อน')
|
|
62
|
-
process.exit(1)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// โครงเก่า (≤0.2.x): workflow/ + warnyin-stages/ ที่ root — เตือนให้ย้ายเอง ไม่แตะงานจริงของ user
|
|
66
|
-
const legacyV2 = ['workflow', 'warnyin-stages'].filter((d) => fs.existsSync(path.join(cwd, d)))
|
|
67
|
-
if (legacyV2.length) {
|
|
68
|
-
console.warn(`⚠ พบโครงเลย์เอาต์เก่า (≤0.2.x): ${legacyV2.join(', ')}
|
|
69
|
-
เวอร์ชันนี้ย้าย core ไปใต้ .warnyin/ และงานจริงไป docs/stages/ — แนะนำย้ายด้วยตัวเองก่อน:
|
|
70
|
-
1. mkdir -p docs/stages && git mv warnyin-stages/* docs/stages/ # งานจริงของคุณ (ปลอดภัย ไม่ถูกแตะโดย installer)
|
|
71
|
-
2. rm -rf workflow warnyin-stages # core เก่า + โฟลเดอร์ที่ย้ายของออกแล้ว
|
|
72
|
-
แล้วรันคำสั่งนี้อีกครั้ง\n`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// โครงเก่า (0.3–0.5.x): ทุกอย่างอยู่ใต้ warnyin/ ที่ root — เวอร์ชันนี้แยกเป็น .warnyin/ (core) + docs/stages (งานจริง)
|
|
76
|
-
const legacyV5 = ['workflow', 'template', 'installer', 'stages'].filter((d) =>
|
|
77
|
-
fs.existsSync(path.join(cwd, 'warnyin', d)),
|
|
78
|
-
)
|
|
79
|
-
if (legacyV5.length) {
|
|
80
|
-
console.warn(`⚠ พบโครงเลย์เอาต์เก่า (0.3–0.5.x): warnyin/{${legacyV5.join(', ')}}
|
|
81
|
-
เวอร์ชันนี้ย้าย core ไป .warnyin/ และงานจริงไป docs/stages/ — แนะนำย้ายด้วยตัวเองก่อน:
|
|
82
|
-
1. mkdir -p docs/stages && git mv warnyin/stages/* docs/stages/ # งานจริงของคุณ (active + achieved) — ปลอดภัย ไม่ถูกแตะ
|
|
83
|
-
2. rm -rf warnyin # core เก่าทั้งหมด (เวอร์ชันใหม่ installer จะวางที่ .warnyin/)
|
|
84
|
-
แล้วรันคำสั่งนี้อีกครั้ง — installer จะวาง .warnyin/ ชุดใหม่ให้\n`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// core = playbook กลาง + command + agent + template — เขียนทับได้เมื่อ --update
|
|
88
|
-
const CORE = [
|
|
89
|
-
path.join('.warnyin', 'workflow'),
|
|
90
|
-
path.join('.warnyin', 'template'),
|
|
91
|
-
path.join('.claude', 'commands', 'warnyin'),
|
|
92
|
-
path.join('.claude', 'agents'),
|
|
93
|
-
path.join('.claude', 'skills'),
|
|
94
|
-
]
|
|
95
|
-
// scaffold = พื้นที่ทำงานเปล่าของโปรเจกต์ — installer "สร้างเอง" ไม่ copy tree จาก package
|
|
96
|
-
// (สำคัญ: ถ้า copy docs/stages จาก pkgRoot งานจริงของ repo ต้นทางจะรั่วไป target ทุกครั้ง — ดู verify installer-test-ci)
|
|
97
|
-
const SCAFFOLD_FILES = [
|
|
98
|
-
path.join('docs', 'stages', 'context.md'), // บริบทงานที่จดไว้ (next/discovery/explore อ่าน "ถ้ามี")
|
|
99
|
-
path.join('docs', 'stages', 'achieved', '.gitkeep'), // ให้ git track โฟลเดอร์ archive เปล่า
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
const stats = { created: 0, updated: 0, skipped: 0 }
|
|
103
|
-
|
|
104
|
-
// target = ปลายทางที่จะเขียนไฟล์ — ตั้งหลัง resolve mode (project=cwd | global=homedir)
|
|
105
|
-
let target = cwd
|
|
106
|
-
|
|
107
|
-
function copyTree(relDir, { overwrite }) {
|
|
108
|
-
const srcDir = path.join(pkgRoot, relDir)
|
|
109
|
-
if (!fs.existsSync(srcDir)) return
|
|
110
|
-
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
111
|
-
const rel = path.join(relDir, entry.name)
|
|
112
|
-
if (entry.isDirectory()) {
|
|
113
|
-
copyTree(rel, { overwrite })
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
116
|
-
const src = path.join(pkgRoot, rel)
|
|
117
|
-
const dest = path.join(target, rel)
|
|
118
|
-
const exists = fs.existsSync(dest)
|
|
119
|
-
if (exists && !overwrite) {
|
|
120
|
-
stats.skipped++
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
if (exists && overwrite && fs.readFileSync(src).equals(fs.readFileSync(dest))) {
|
|
124
|
-
stats.skipped++
|
|
125
|
-
continue
|
|
126
|
-
}
|
|
127
|
-
if (!DRY) {
|
|
128
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
129
|
-
fs.copyFileSync(src, dest)
|
|
130
|
-
}
|
|
131
|
-
stats[exists ? 'updated' : 'created']++
|
|
132
|
-
console.log(` ${exists ? '↻' : '+'} ${rel}`)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** สร้างโครง scaffold เปล่าของ docs/stages เอง (ไม่ copy จาก package — กันงานจริงของ repo ต้นทางรั่วไป target) — ไม่ทับไฟล์ที่มีอยู่ */
|
|
137
|
-
function ensureScaffold() {
|
|
138
|
-
for (const rel of SCAFFOLD_FILES) {
|
|
139
|
-
const dest = path.join(target, rel)
|
|
140
|
-
if (fs.existsSync(dest)) {
|
|
141
|
-
stats.skipped++
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
if (!DRY) {
|
|
145
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
146
|
-
fs.writeFileSync(dest, '')
|
|
147
|
-
}
|
|
148
|
-
stats.created++
|
|
149
|
-
console.log(` + ${rel}`)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** seed docs/ ของโปรเจกต์จาก .warnyin/template/docs — ข้ามโฟลเดอร์ template `[...]` (ไว้ให้ /warnyin:init copy เป็นชื่อจริง) และไม่ทับไฟล์ที่มีอยู่ */
|
|
154
|
-
const TEMPLATE_DOCS = path.join('.warnyin', 'template', 'docs')
|
|
155
|
-
function seedDocs(relDir = TEMPLATE_DOCS) {
|
|
156
|
-
const srcDir = path.join(pkgRoot, relDir)
|
|
157
|
-
if (!fs.existsSync(srcDir)) return
|
|
158
|
-
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
159
|
-
if (entry.name.startsWith('[')) continue
|
|
160
|
-
const rel = path.join(relDir, entry.name)
|
|
161
|
-
if (entry.isDirectory()) {
|
|
162
|
-
seedDocs(rel)
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
165
|
-
const destRel = path.join('docs', path.relative(TEMPLATE_DOCS, rel))
|
|
166
|
-
const dest = path.join(target, destRel)
|
|
167
|
-
if (fs.existsSync(dest)) {
|
|
168
|
-
stats.skipped++
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
if (!DRY) {
|
|
172
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
173
|
-
fs.copyFileSync(path.join(pkgRoot, rel), dest)
|
|
174
|
-
}
|
|
175
|
-
stats.created++
|
|
176
|
-
console.log(` + ${destRel}`)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** CLAUDE.md / AGENTS.md: ไม่มี → สร้างจาก template; มีอยู่แล้วแต่ยังไม่มี workflow → ต่อท้ายเป็น section */
|
|
181
|
-
function installRootDoc(name, srcPath) {
|
|
182
|
-
const dest = path.join(target, name)
|
|
183
|
-
const content = fs.readFileSync(srcPath, 'utf8')
|
|
184
|
-
if (!fs.existsSync(dest)) {
|
|
185
|
-
if (!DRY) fs.writeFileSync(dest, content)
|
|
186
|
-
stats.created++
|
|
187
|
-
console.log(` + ${name}`)
|
|
188
|
-
return
|
|
189
|
-
}
|
|
190
|
-
const existing = fs.readFileSync(dest, 'utf8')
|
|
191
|
-
if (existing.includes('warnyin/workflow/stages/')) {
|
|
192
|
-
stats.skipped++
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
const section = '\n\n' + content.replace(/^#\s[^\n]*\n/, '## Warnyin Standard Workflow\n')
|
|
196
|
-
if (!DRY) fs.appendFileSync(dest, section)
|
|
197
|
-
stats.updated++
|
|
198
|
-
console.log(` ± ${name} (ต่อท้าย section Warnyin Standard Workflow)`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const GLOBAL_NOTE_MARKER = '<!-- warnyin:global-note -->'
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* เขียน resolution note ลง ~/.claude/CLAUDE.md แบบ note-only append-with-marker (global mode)
|
|
205
|
-
* — ห้ามเขียนทับทั้งไฟล์ (personal global memory ของ user); append เฉพาะถ้ายังไม่มี marker (idempotent)
|
|
206
|
-
* — defensive skip ถ้า template ไม่มี (worktree T1 เดี่ยวก่อน merge T2) — pattern เดียวกับ copyTree/seedDocs
|
|
207
|
-
*/
|
|
208
|
-
function installGlobalNote() {
|
|
209
|
-
const src = path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.global.md')
|
|
210
|
-
const destRel = path.join('.claude', 'CLAUDE.md')
|
|
211
|
-
const dest = path.join(target, destRel)
|
|
212
|
-
if (!fs.existsSync(src)) {
|
|
213
|
-
console.log(` · ข้าม ${destRel} (ยังไม่มี template CLAUDE.global.md)`)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
const note = fs.readFileSync(src, 'utf8')
|
|
217
|
-
if (!fs.existsSync(dest)) {
|
|
218
|
-
if (!DRY) {
|
|
219
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
220
|
-
fs.writeFileSync(dest, note)
|
|
221
|
-
}
|
|
222
|
-
stats.created++
|
|
223
|
-
console.log(` + ${destRel}`)
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
const existing = fs.readFileSync(dest, 'utf8')
|
|
227
|
-
if (existing.includes(GLOBAL_NOTE_MARKER)) {
|
|
228
|
-
stats.skipped++
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
const section = (existing.endsWith('\n') ? '\n' : '\n\n') + note
|
|
232
|
-
if (!DRY) fs.appendFileSync(dest, section)
|
|
233
|
-
stats.updated++
|
|
234
|
-
console.log(` ± ${destRel} (ต่อท้าย warnyin global note)`)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** เลือก mode + ติดตั้งตาม mode (target=cwd|homedir). ห่อ async เฉพาะ path TTY (readline) */
|
|
238
|
-
async function main() {
|
|
239
|
-
const globalFlag = args.has('--global')
|
|
240
|
-
const projectFlag = args.has('--project')
|
|
241
|
-
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
242
|
-
|
|
243
|
-
let mode
|
|
244
|
-
try {
|
|
245
|
-
if (!globalFlag && !projectFlag && isTTY) {
|
|
246
|
-
// ถาม เฉพาะ path TTY — non-TTY/flag ไม่แตะ readline (ไม่ค้าง/ไม่ช้าลง)
|
|
247
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
248
|
-
let answer
|
|
249
|
-
try {
|
|
250
|
-
answer = await rl.question('ติดตั้งแบบไหน? [1] โปรเจกต์นี้ (default) [2] global (~/) : ')
|
|
251
|
-
} finally {
|
|
252
|
-
rl.close()
|
|
253
|
-
}
|
|
254
|
-
mode = resolveMode({ globalFlag, projectFlag, isTTY, answer })
|
|
255
|
-
} else {
|
|
256
|
-
mode = resolveMode({ globalFlag, projectFlag, isTTY })
|
|
257
|
-
}
|
|
258
|
-
} catch (e) {
|
|
259
|
-
console.error(`✖ ${e.message}`)
|
|
260
|
-
process.exit(1)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (mode === 'global') {
|
|
264
|
-
const home = os.homedir()
|
|
265
|
-
// homedir guard — falsy หรือ filesystem root (CI/container ไม่มี passwd) → error แทนเขียนลง root
|
|
266
|
-
if (!home || path.resolve(home) === path.parse(path.resolve(home)).root) {
|
|
267
|
-
console.error('✖ หา home directory ไม่ได้ (หรือเป็น filesystem root) — ใช้ --project แทน')
|
|
268
|
-
process.exit(1)
|
|
269
|
-
}
|
|
270
|
-
target = home
|
|
271
|
-
console.log(`Warnyin Standard Workflow → global ${target}${DRY ? ' (dry-run)' : ''}`)
|
|
272
|
-
console.log(` จะเขียนนอกโปรเจกต์ที่: ${path.join(target, '.warnyin')}, ${path.join(target, '.claude', 'commands', 'warnyin')}, ${path.join(target, '.claude', 'CLAUDE.md')}\n`)
|
|
273
|
-
// first-install overwrite:false (skip ของเดิมใน ~/.claude/{agents,skills}) — ทับเฉพาะ --global --update
|
|
274
|
-
for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
|
|
275
|
-
// ข้าม scaffold/seedDocs (ยกให้ /warnyin:init) + ข้าม AGENTS.md global (DQ3 limitation)
|
|
276
|
-
installGlobalNote()
|
|
277
|
-
} else {
|
|
278
|
-
target = cwd
|
|
279
|
-
console.log(`Warnyin Standard Workflow → ${target}${DRY ? ' (dry-run)' : ''}\n`)
|
|
280
|
-
for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
|
|
281
|
-
ensureScaffold()
|
|
282
|
-
seedDocs()
|
|
283
|
-
installRootDoc('CLAUDE.md', path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.md'))
|
|
284
|
-
installRootDoc('AGENTS.md', path.join(pkgRoot, 'AGENTS.md'))
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
console.log(`\nสรุป: สร้างใหม่ ${stats.created} · อัปเดต ${stats.updated} · ข้าม (มีอยู่แล้ว) ${stats.skipped}`)
|
|
288
|
-
|
|
289
|
-
if (!UPDATE) {
|
|
290
|
-
if (mode === 'global') {
|
|
291
|
-
console.log(`
|
|
292
|
-
ติดตั้ง global แล้ว — /warnyin:* ใช้ได้ทุกโปรเจกต์ (จาก ~/.claude/commands/)
|
|
293
|
-
เปิดโปรเจกต์ใด ๆ ใน Claude Code → รัน /warnyin:init เพื่อสร้าง workspace (docs/) ของโปรเจกต์นั้น
|
|
294
|
-
|
|
295
|
-
อัปเดต playbook ภายหลัง: npx @warnyin/agents --global --update`)
|
|
296
|
-
} else {
|
|
297
|
-
console.log(`
|
|
298
|
-
ขั้นถัดไป:
|
|
299
|
-
1. เปิด Claude Code ในโปรเจกต์นี้ แล้วรัน /warnyin:init — ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/
|
|
300
|
-
2. เริ่มงานแรก: /warnyin:discovery <topic> หรือ /warnyin:design <slug> <change>
|
|
301
|
-
|
|
302
|
-
อัปเดต playbook ภายหลัง: npx @warnyin/agents --update`)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// main-guard: รันเฉพาะตอน execute ตรง ๆ (ไม่ trigger ตอน import เพื่อ unit-test resolveMode)
|
|
308
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
309
|
-
await main()
|
|
310
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* warnyin-agents installer
|
|
4
|
+
* ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน (cwd) หรือแบบ global (~/)
|
|
5
|
+
*
|
|
6
|
+
* npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ข้ามไฟล์ที่มีอยู่แล้ว)
|
|
7
|
+
* npx @warnyin/agents --global ติดตั้งแบบ global (~/) ใช้ได้ทุกโปรเจกต์
|
|
8
|
+
* npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ — ไม่ถาม)
|
|
9
|
+
* npx @warnyin/agents --update อัปเดต playbook กลาง (เขียนทับเฉพาะไฟล์ core)
|
|
10
|
+
* npx @warnyin/agents --dry-run แสดงว่าจะทำอะไร โดยไม่เขียนไฟล์จริง
|
|
11
|
+
* (ทางสำรองไม่ผ่าน npm: npx github:warnyin/warnyin-agents)
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import os from 'node:os'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import { createInterface } from 'node:readline/promises'
|
|
18
|
+
|
|
19
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const cwd = process.cwd()
|
|
21
|
+
const args = new Set(process.argv.slice(2))
|
|
22
|
+
const UPDATE = args.has('--update')
|
|
23
|
+
const DRY = args.has('--dry-run')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ★ pure function — เลือก mode จาก flag/TTY/answer (ไม่มี side-effect, export ให้ unit test)
|
|
27
|
+
* @param {{globalFlag?:boolean, projectFlag?:boolean, isTTY?:boolean, answer?:string}} o
|
|
28
|
+
* @returns {'project'|'global'}
|
|
29
|
+
*/
|
|
30
|
+
export function resolveMode({ globalFlag, projectFlag, isTTY, answer } = {}) {
|
|
31
|
+
if (globalFlag && projectFlag) {
|
|
32
|
+
throw new Error('--global กับ --project ใช้พร้อมกันไม่ได้ (เลือกอย่างใดอย่างหนึ่ง)')
|
|
33
|
+
}
|
|
34
|
+
if (globalFlag) return 'global'
|
|
35
|
+
if (projectFlag) return 'project'
|
|
36
|
+
if (!isTTY) return 'project' // CI-safe default — npx/pipe ไม่ค้างรอ input
|
|
37
|
+
const a = (answer ?? '').trim().toLowerCase()
|
|
38
|
+
return a === '2' || a === 'global' ? 'global' : 'project'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (args.has('--help') || args.has('-h')) {
|
|
42
|
+
console.log(`warnyin-agents — ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน หรือแบบ global
|
|
43
|
+
|
|
44
|
+
ใช้งาน:
|
|
45
|
+
npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ถ้า TTY จะถามก่อน; ข้ามไฟล์ที่มีอยู่แล้ว)
|
|
46
|
+
npx @warnyin/agents --global ติดตั้งแบบ global ลง ~/ (~/.warnyin + ~/.claude) ใช้ได้ทุกโปรเจกต์
|
|
47
|
+
npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ ไม่ถาม)
|
|
48
|
+
npx @warnyin/agents --update อัปเดต playbook กลางเป็นเวอร์ชันล่าสุด
|
|
49
|
+
(เขียนทับเฉพาะ .warnyin/workflow/, .claude/commands/warnyin/,
|
|
50
|
+
template .warnyin/template/stages/[topic] — ไม่แตะ docs/ และงานจริง)
|
|
51
|
+
npx @warnyin/agents --dry-run แสดงรายการไฟล์ที่จะสร้าง/อัปเดต โดยไม่เขียนจริง
|
|
52
|
+
|
|
53
|
+
หลังติดตั้ง: เปิด Claude Code ในโปรเจกต์ แล้วรัน /warnyin:init ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/`)
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// guard กัน self-install — เก็บไว้แบบ defensive (zero-cost)
|
|
58
|
+
// หลังย้าย source เข้า src/ → pkgRoot = src/ (sibling ของ bin/) จึงแทบไม่มีทาง === cwd (repo root / temp sandbox)
|
|
59
|
+
// → guard นี้เป็น no-op โดยตั้งใจในเคสปกติ/sandbox; ยังคงดักได้เฉพาะ edge case ที่ install ลงโฟลเดอร์ที่เป็น src/ เอง
|
|
60
|
+
if (path.resolve(pkgRoot) === path.resolve(cwd)) {
|
|
61
|
+
console.error('✖ กำลังรันอยู่ใน repo ของ warnyin-agents เอง — ให้ cd ไปที่โปรเจกต์ปลายทางก่อน')
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// โครงเก่า (≤0.2.x): workflow/ + warnyin-stages/ ที่ root — เตือนให้ย้ายเอง ไม่แตะงานจริงของ user
|
|
66
|
+
const legacyV2 = ['workflow', 'warnyin-stages'].filter((d) => fs.existsSync(path.join(cwd, d)))
|
|
67
|
+
if (legacyV2.length) {
|
|
68
|
+
console.warn(`⚠ พบโครงเลย์เอาต์เก่า (≤0.2.x): ${legacyV2.join(', ')}
|
|
69
|
+
เวอร์ชันนี้ย้าย core ไปใต้ .warnyin/ และงานจริงไป docs/stages/ — แนะนำย้ายด้วยตัวเองก่อน:
|
|
70
|
+
1. mkdir -p docs/stages && git mv warnyin-stages/* docs/stages/ # งานจริงของคุณ (ปลอดภัย ไม่ถูกแตะโดย installer)
|
|
71
|
+
2. rm -rf workflow warnyin-stages # core เก่า + โฟลเดอร์ที่ย้ายของออกแล้ว
|
|
72
|
+
แล้วรันคำสั่งนี้อีกครั้ง\n`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// โครงเก่า (0.3–0.5.x): ทุกอย่างอยู่ใต้ warnyin/ ที่ root — เวอร์ชันนี้แยกเป็น .warnyin/ (core) + docs/stages (งานจริง)
|
|
76
|
+
const legacyV5 = ['workflow', 'template', 'installer', 'stages'].filter((d) =>
|
|
77
|
+
fs.existsSync(path.join(cwd, 'warnyin', d)),
|
|
78
|
+
)
|
|
79
|
+
if (legacyV5.length) {
|
|
80
|
+
console.warn(`⚠ พบโครงเลย์เอาต์เก่า (0.3–0.5.x): warnyin/{${legacyV5.join(', ')}}
|
|
81
|
+
เวอร์ชันนี้ย้าย core ไป .warnyin/ และงานจริงไป docs/stages/ — แนะนำย้ายด้วยตัวเองก่อน:
|
|
82
|
+
1. mkdir -p docs/stages && git mv warnyin/stages/* docs/stages/ # งานจริงของคุณ (active + achieved) — ปลอดภัย ไม่ถูกแตะ
|
|
83
|
+
2. rm -rf warnyin # core เก่าทั้งหมด (เวอร์ชันใหม่ installer จะวางที่ .warnyin/)
|
|
84
|
+
แล้วรันคำสั่งนี้อีกครั้ง — installer จะวาง .warnyin/ ชุดใหม่ให้\n`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// core = playbook กลาง + command + agent + template — เขียนทับได้เมื่อ --update
|
|
88
|
+
const CORE = [
|
|
89
|
+
path.join('.warnyin', 'workflow'),
|
|
90
|
+
path.join('.warnyin', 'template'),
|
|
91
|
+
path.join('.claude', 'commands', 'warnyin'),
|
|
92
|
+
path.join('.claude', 'agents'),
|
|
93
|
+
path.join('.claude', 'skills'),
|
|
94
|
+
]
|
|
95
|
+
// scaffold = พื้นที่ทำงานเปล่าของโปรเจกต์ — installer "สร้างเอง" ไม่ copy tree จาก package
|
|
96
|
+
// (สำคัญ: ถ้า copy docs/stages จาก pkgRoot งานจริงของ repo ต้นทางจะรั่วไป target ทุกครั้ง — ดู verify installer-test-ci)
|
|
97
|
+
const SCAFFOLD_FILES = [
|
|
98
|
+
path.join('docs', 'stages', 'context.md'), // บริบทงานที่จดไว้ (next/discovery/explore อ่าน "ถ้ามี")
|
|
99
|
+
path.join('docs', 'stages', 'achieved', '.gitkeep'), // ให้ git track โฟลเดอร์ archive เปล่า
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
const stats = { created: 0, updated: 0, skipped: 0 }
|
|
103
|
+
|
|
104
|
+
// target = ปลายทางที่จะเขียนไฟล์ — ตั้งหลัง resolve mode (project=cwd | global=homedir)
|
|
105
|
+
let target = cwd
|
|
106
|
+
|
|
107
|
+
function copyTree(relDir, { overwrite }) {
|
|
108
|
+
const srcDir = path.join(pkgRoot, relDir)
|
|
109
|
+
if (!fs.existsSync(srcDir)) return
|
|
110
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
111
|
+
const rel = path.join(relDir, entry.name)
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
copyTree(rel, { overwrite })
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
const src = path.join(pkgRoot, rel)
|
|
117
|
+
const dest = path.join(target, rel)
|
|
118
|
+
const exists = fs.existsSync(dest)
|
|
119
|
+
if (exists && !overwrite) {
|
|
120
|
+
stats.skipped++
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (exists && overwrite && fs.readFileSync(src).equals(fs.readFileSync(dest))) {
|
|
124
|
+
stats.skipped++
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
if (!DRY) {
|
|
128
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
129
|
+
fs.copyFileSync(src, dest)
|
|
130
|
+
}
|
|
131
|
+
stats[exists ? 'updated' : 'created']++
|
|
132
|
+
console.log(` ${exists ? '↻' : '+'} ${rel}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** สร้างโครง scaffold เปล่าของ docs/stages เอง (ไม่ copy จาก package — กันงานจริงของ repo ต้นทางรั่วไป target) — ไม่ทับไฟล์ที่มีอยู่ */
|
|
137
|
+
function ensureScaffold() {
|
|
138
|
+
for (const rel of SCAFFOLD_FILES) {
|
|
139
|
+
const dest = path.join(target, rel)
|
|
140
|
+
if (fs.existsSync(dest)) {
|
|
141
|
+
stats.skipped++
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (!DRY) {
|
|
145
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
146
|
+
fs.writeFileSync(dest, '')
|
|
147
|
+
}
|
|
148
|
+
stats.created++
|
|
149
|
+
console.log(` + ${rel}`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** seed docs/ ของโปรเจกต์จาก .warnyin/template/docs — ข้ามโฟลเดอร์ template `[...]` (ไว้ให้ /warnyin:init copy เป็นชื่อจริง) และไม่ทับไฟล์ที่มีอยู่ */
|
|
154
|
+
const TEMPLATE_DOCS = path.join('.warnyin', 'template', 'docs')
|
|
155
|
+
function seedDocs(relDir = TEMPLATE_DOCS) {
|
|
156
|
+
const srcDir = path.join(pkgRoot, relDir)
|
|
157
|
+
if (!fs.existsSync(srcDir)) return
|
|
158
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
159
|
+
if (entry.name.startsWith('[')) continue
|
|
160
|
+
const rel = path.join(relDir, entry.name)
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
seedDocs(rel)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
const destRel = path.join('docs', path.relative(TEMPLATE_DOCS, rel))
|
|
166
|
+
const dest = path.join(target, destRel)
|
|
167
|
+
if (fs.existsSync(dest)) {
|
|
168
|
+
stats.skipped++
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (!DRY) {
|
|
172
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
173
|
+
fs.copyFileSync(path.join(pkgRoot, rel), dest)
|
|
174
|
+
}
|
|
175
|
+
stats.created++
|
|
176
|
+
console.log(` + ${destRel}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** CLAUDE.md / AGENTS.md: ไม่มี → สร้างจาก template; มีอยู่แล้วแต่ยังไม่มี workflow → ต่อท้ายเป็น section */
|
|
181
|
+
function installRootDoc(name, srcPath) {
|
|
182
|
+
const dest = path.join(target, name)
|
|
183
|
+
const content = fs.readFileSync(srcPath, 'utf8')
|
|
184
|
+
if (!fs.existsSync(dest)) {
|
|
185
|
+
if (!DRY) fs.writeFileSync(dest, content)
|
|
186
|
+
stats.created++
|
|
187
|
+
console.log(` + ${name}`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const existing = fs.readFileSync(dest, 'utf8')
|
|
191
|
+
if (existing.includes('warnyin/workflow/stages/')) {
|
|
192
|
+
stats.skipped++
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
const section = '\n\n' + content.replace(/^#\s[^\n]*\n/, '## Warnyin Standard Workflow\n')
|
|
196
|
+
if (!DRY) fs.appendFileSync(dest, section)
|
|
197
|
+
stats.updated++
|
|
198
|
+
console.log(` ± ${name} (ต่อท้าย section Warnyin Standard Workflow)`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const GLOBAL_NOTE_MARKER = '<!-- warnyin:global-note -->'
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* เขียน resolution note ลง ~/.claude/CLAUDE.md แบบ note-only append-with-marker (global mode)
|
|
205
|
+
* — ห้ามเขียนทับทั้งไฟล์ (personal global memory ของ user); append เฉพาะถ้ายังไม่มี marker (idempotent)
|
|
206
|
+
* — defensive skip ถ้า template ไม่มี (worktree T1 เดี่ยวก่อน merge T2) — pattern เดียวกับ copyTree/seedDocs
|
|
207
|
+
*/
|
|
208
|
+
function installGlobalNote() {
|
|
209
|
+
const src = path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.global.md')
|
|
210
|
+
const destRel = path.join('.claude', 'CLAUDE.md')
|
|
211
|
+
const dest = path.join(target, destRel)
|
|
212
|
+
if (!fs.existsSync(src)) {
|
|
213
|
+
console.log(` · ข้าม ${destRel} (ยังไม่มี template CLAUDE.global.md)`)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
const note = fs.readFileSync(src, 'utf8')
|
|
217
|
+
if (!fs.existsSync(dest)) {
|
|
218
|
+
if (!DRY) {
|
|
219
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
220
|
+
fs.writeFileSync(dest, note)
|
|
221
|
+
}
|
|
222
|
+
stats.created++
|
|
223
|
+
console.log(` + ${destRel}`)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const existing = fs.readFileSync(dest, 'utf8')
|
|
227
|
+
if (existing.includes(GLOBAL_NOTE_MARKER)) {
|
|
228
|
+
stats.skipped++
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
const section = (existing.endsWith('\n') ? '\n' : '\n\n') + note
|
|
232
|
+
if (!DRY) fs.appendFileSync(dest, section)
|
|
233
|
+
stats.updated++
|
|
234
|
+
console.log(` ± ${destRel} (ต่อท้าย warnyin global note)`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** เลือก mode + ติดตั้งตาม mode (target=cwd|homedir). ห่อ async เฉพาะ path TTY (readline) */
|
|
238
|
+
async function main() {
|
|
239
|
+
const globalFlag = args.has('--global')
|
|
240
|
+
const projectFlag = args.has('--project')
|
|
241
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
242
|
+
|
|
243
|
+
let mode
|
|
244
|
+
try {
|
|
245
|
+
if (!globalFlag && !projectFlag && isTTY) {
|
|
246
|
+
// ถาม เฉพาะ path TTY — non-TTY/flag ไม่แตะ readline (ไม่ค้าง/ไม่ช้าลง)
|
|
247
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
248
|
+
let answer
|
|
249
|
+
try {
|
|
250
|
+
answer = await rl.question('ติดตั้งแบบไหน? [1] โปรเจกต์นี้ (default) [2] global (~/) : ')
|
|
251
|
+
} finally {
|
|
252
|
+
rl.close()
|
|
253
|
+
}
|
|
254
|
+
mode = resolveMode({ globalFlag, projectFlag, isTTY, answer })
|
|
255
|
+
} else {
|
|
256
|
+
mode = resolveMode({ globalFlag, projectFlag, isTTY })
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.error(`✖ ${e.message}`)
|
|
260
|
+
process.exit(1)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (mode === 'global') {
|
|
264
|
+
const home = os.homedir()
|
|
265
|
+
// homedir guard — falsy หรือ filesystem root (CI/container ไม่มี passwd) → error แทนเขียนลง root
|
|
266
|
+
if (!home || path.resolve(home) === path.parse(path.resolve(home)).root) {
|
|
267
|
+
console.error('✖ หา home directory ไม่ได้ (หรือเป็น filesystem root) — ใช้ --project แทน')
|
|
268
|
+
process.exit(1)
|
|
269
|
+
}
|
|
270
|
+
target = home
|
|
271
|
+
console.log(`Warnyin Standard Workflow → global ${target}${DRY ? ' (dry-run)' : ''}`)
|
|
272
|
+
console.log(` จะเขียนนอกโปรเจกต์ที่: ${path.join(target, '.warnyin')}, ${path.join(target, '.claude', 'commands', 'warnyin')}, ${path.join(target, '.claude', 'CLAUDE.md')}\n`)
|
|
273
|
+
// first-install overwrite:false (skip ของเดิมใน ~/.claude/{agents,skills}) — ทับเฉพาะ --global --update
|
|
274
|
+
for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
|
|
275
|
+
// ข้าม scaffold/seedDocs (ยกให้ /warnyin:init) + ข้าม AGENTS.md global (DQ3 limitation)
|
|
276
|
+
installGlobalNote()
|
|
277
|
+
} else {
|
|
278
|
+
target = cwd
|
|
279
|
+
console.log(`Warnyin Standard Workflow → ${target}${DRY ? ' (dry-run)' : ''}\n`)
|
|
280
|
+
for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
|
|
281
|
+
ensureScaffold()
|
|
282
|
+
seedDocs()
|
|
283
|
+
installRootDoc('CLAUDE.md', path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.md'))
|
|
284
|
+
installRootDoc('AGENTS.md', path.join(pkgRoot, 'AGENTS.md'))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`\nสรุป: สร้างใหม่ ${stats.created} · อัปเดต ${stats.updated} · ข้าม (มีอยู่แล้ว) ${stats.skipped}`)
|
|
288
|
+
|
|
289
|
+
if (!UPDATE) {
|
|
290
|
+
if (mode === 'global') {
|
|
291
|
+
console.log(`
|
|
292
|
+
ติดตั้ง global แล้ว — /warnyin:* ใช้ได้ทุกโปรเจกต์ (จาก ~/.claude/commands/)
|
|
293
|
+
เปิดโปรเจกต์ใด ๆ ใน Claude Code → รัน /warnyin:init เพื่อสร้าง workspace (docs/) ของโปรเจกต์นั้น
|
|
294
|
+
|
|
295
|
+
อัปเดต playbook ภายหลัง: npx @warnyin/agents --global --update`)
|
|
296
|
+
} else {
|
|
297
|
+
console.log(`
|
|
298
|
+
ขั้นถัดไป:
|
|
299
|
+
1. เปิด Claude Code ในโปรเจกต์นี้ แล้วรัน /warnyin:init — ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/
|
|
300
|
+
2. เริ่มงานแรก: /warnyin:discovery <topic> หรือ /warnyin:design <slug> <change>
|
|
301
|
+
|
|
302
|
+
อัปเดต playbook ภายหลัง: npx @warnyin/agents --update`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// main-guard: รันเฉพาะตอน execute ตรง ๆ (ไม่ trigger ตอน import เพื่อ unit-test resolveMode)
|
|
308
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
309
|
+
await main()
|
|
310
|
+
}
|