@warnyin/agents 0.12.0 → 0.13.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 CHANGED
@@ -23,6 +23,11 @@
23
23
 
24
24
  ## [Unreleased]
25
25
 
26
+ ## [0.13.0] - 2026-06-11
27
+
28
+ ### Added
29
+ - **Global install mode — ติดตั้งครั้งเดียวใช้ได้ทุกโปรเจกต์ (opt-in)** (feature `global-install`) — `npx @warnyin/agents --global` ติดตั้ง adapter → `~/.claude/{commands/warnyin,agents,skills}` + playbook → `~/.warnyin/{workflow,template}` **ครั้งเดียว** → `/warnyin:*` ใช้ได้ทุกโปรเจกต์ (Claude Code โหลด user-level `~/.claude/`). **Hybrid:** workspace (`docs/`) ยัง per-project; โปรเจกต์ที่มี `./.warnyin/` local → ใช้ local ก่อน (override → คง reproducibility); **per-project ยังเป็น default**. **mode resolution** `resolveMode()` (pure-fn): flag `--global`/`--project`; ไม่ระบุ+TTY → prompt; **non-TTY → project (CI-safe ไม่ค้าง)**; `--global --project` → error. **ปลอดภัยต่อ homedir:** first-install `overwrite:false` (ไม่ทับไฟล์ user ใน `~/.claude/{agents,skills}`), `installGlobalNote()` เขียน `~/.claude/CLAUDE.md` แบบ **append-with-marker** (ไม่แตะ personal global memory), homedir guard (falsy/root → error), echo target paths. **resolve playbook local-first → global** ผ่าน convention canonical ใน `CLAUDE.md`/`AGENTS.md`/`CLAUDE.global.md` (ไม่ duplicate ลงทุก adapter). `/warnyin:init` รับ workspace bootstrap (scaffold + seed `docs/`, อ่าน template local→global). **backward compatible** (project mode = default ไม่เปลี่ยน; `--global` opt-in). **limitation:** Codex/Antigravity global root doc รอบนี้ยังไม่รองรับ (per-project ใช้ได้เต็ม). zero-dep + cross-platform (`os.homedir()`, HOME/USERPROFILE); payload ติดมากับ `--update` รอบถัดไป
30
+
26
31
  ## [0.12.0] - 2026-06-11
27
32
 
28
33
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warnyin/agents",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Warnyin Standard Workflow installer — 5-stage ways of work (Discovery/DESIGN/BUILD/VERIFY/SHIP) สำหรับทุกโปรเจกต์",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,5 @@
1
+ <!-- warnyin:global-note -->
2
+
3
+ ## การ resolve playbook (local-first → global)
4
+ - path `.warnyin/workflow/...` / `.warnyin/template/...`: หาในโปรเจกต์ `./.warnyin/` ก่อน ไม่มี → `~/.warnyin/` (global install)
5
+ - ถ้ายังไม่มี `docs/stages/` (global mode โปรเจกต์ใหม่) → รัน `/warnyin:init` ก่อน (สร้าง workspace)
@@ -28,3 +28,7 @@ Claude Code อ่าน `.claude/` + ไฟล์นี้, ส่วน Codex/
28
28
 
29
29
  ## อัปเดต workflow
30
30
  `npx @warnyin/agents --update` — เขียนทับเฉพาะ playbook กลาง (`.warnyin/workflow/`, `.claude/commands/warnyin/`, template `.warnyin/template/stages/[topic]/`) ไม่แตะ `docs/` และงานจริงใน `docs/stages/`
31
+
32
+ ## การ resolve playbook (local-first → global)
33
+ - path `.warnyin/workflow/...` / `.warnyin/template/...`: หาในโปรเจกต์ `./.warnyin/` ก่อน ไม่มี → `~/.warnyin/` (global install)
34
+ - ถ้ายังไม่มี `docs/stages/` (global mode โปรเจกต์ใหม่) → รัน `/warnyin:init` ก่อน (สร้าง workspace)
@@ -31,6 +31,16 @@
31
31
 
32
32
  ## 3. ลำดับขั้นการทำงาน (process)
33
33
 
34
+ 0. **Workspace bootstrap (ทำก่อนวิเคราะห์โปรเจกต์ — idempotent):**
35
+ - **หา template:** ตรวจ `./.warnyin/template/` ก่อน (local) — ไม่มีหรือไม่มี `./.warnyin/` → fallback `~/.warnyin/template/` (global install); ใช้ path ที่พบเป็น `<template>`
36
+ - **สร้าง scaffold (ถ้ายังไม่มี):**
37
+ - `docs/stages/context.md` — ไม่มีให้สร้างไฟล์เปล่า; มีอยู่แล้ว → ข้าม
38
+ - `docs/stages/achieved/.gitkeep` — ไม่มีให้สร้างไฟล์เปล่า (สร้าง dir `docs/stages/achieved/` ด้วยถ้าจำเป็น); มีอยู่แล้ว → ข้าม
39
+ - **seed `docs/` จาก `<template>/docs/**` (ถ้า template มี):**
40
+ - วน entry ใน `<template>/docs/` — **ข้าม entry ที่ชื่อขึ้นต้นด้วย `[`** (placeholder เช่น `[component]/`, `[topic]/` — seedDocs-skip invariant)
41
+ - entry ที่เหลือ: ปลายทาง `docs/<entry>` — **ไม่ทับไฟล์ที่มีอยู่แล้ว** (copy เฉพาะที่ยังไม่มี)
42
+ - **ทั้งหมดนี้เป็น agent-driven** — agent ตรวจ/สร้างไฟล์เปล่าเอง; ไม่ต้องรัน script
43
+
34
44
  1. **สแกนภาพรวม:** โครงสร้าง repo, package manifest, ภาษา/framework, แบ่งเป็น **component** อะไรบ้าง (เช่น api-service, admin-console)
35
45
  2. **วิเคราะห์ลึกต่อ component (ขนานได้, read-only):** โครงสร้างโฟลเดอร์/โมดูล, pattern/convention ที่ใช้จริงในโค้ด, วิธี build/test ที่มีอยู่
36
46
  3. **วิเคราะห์ infra:** docker/compose, env, service ที่ต้องรันสำหรับ local dev
@@ -44,7 +54,7 @@
44
54
  5. **เสนอ summary → user ยืนยันครั้งเดียว**
45
55
  6. **เขียนไฟล์จริงลง `docs/` ให้ครบตาราง §4 (ขั้นบังคับ ห้ามข้าม)** — ทำตามกลไก 6.1–6.4 นี้:
46
56
 
47
- **6.1 ไฟล์ root** — copy template แล้วเติม:
57
+ **6.1 ไฟล์ root** — copy template แล้วเติม (ขั้นนี้ = เติมเนื้อหาหลังวิเคราะห์ เสริมขั้น 0 ที่สร้าง scaffold/seed ไว้แล้ว):
48
58
  ```
49
59
  mkdir -p docs
50
60
  cp .warnyin/template/docs/project.md docs/project.md
@@ -52,6 +62,7 @@
52
62
  cp .warnyin/template/docs/rule.md docs/rule.md
53
63
  cp .warnyin/template/docs/troubleshooting.md docs/troubleshooting.md
54
64
  ```
65
+ - ใช้ `.warnyin/template/` (local) — per-project install มีให้เสมอ; global mode ขั้น 0 seed ให้แล้ว (template อ่าน local→global ตามขั้น 0)
55
66
  - ไฟล์ไหนมีอยู่แล้วใน `docs/` → **ห้าม `cp` ทับ** ให้เปิดอ่านแล้ว Edit เติมแทน
56
67
  - `project.md` → เติมจากผลสัมภาษณ์ user (ข้อ 4) · `infra.md` → เติมจาก config จริง (ข้อ 3) · `rule.md`/`troubleshooting.md` → วางโครงหัวข้อ ใส่ `<!-- ยังว่าง รอเติม -->` ในส่วนที่ยังไม่มีข้อมูล
57
68
 
package/src/AGENTS.md CHANGED
@@ -46,3 +46,9 @@ Discovery (optional) ──▶ DESIGN ──▶ BUILD ──▶ VERIFY ──▶
46
46
  ทำตาม `.warnyin/workflow/stages/discovery.md` — เริ่มอ่าน `docs/project.md`, ตี scope กว้าง→แคบ,
47
47
  ถามทีละข้อพร้อมเสนอคำตอบที่แนะนำ, คำถามที่ตอบได้ด้วยโค้ดให้ไปอ่านโค้ดเอง,
48
48
  เขียน output ลง `docs/stages/<slug>/discovery.md` และ `research.md`
49
+
50
+ ## การ resolve playbook (local-first → global)
51
+ - path `.warnyin/workflow/...` / `.warnyin/template/...`: หาในโปรเจกต์ `./.warnyin/` ก่อน ไม่มี → `~/.warnyin/` (global install)
52
+ - ถ้ายังไม่มี `docs/stages/` (global mode โปรเจกต์ใหม่) → รัน `/warnyin:init` ก่อน (สร้าง workspace)
53
+
54
+ > หมายเหตุ: global root doc ของ Codex/Antigravity ไม่รองรับรอบนี้ — convention นี้มีผลเฉพาะ per-project path
package/src/bin/cli.mjs CHANGED
@@ -1,28 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * warnyin-agents installer
4
- * ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน (cwd)
4
+ * ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน (cwd) หรือแบบ global (~/)
5
5
  *
6
- * npx @warnyin/agents ติดตั้ง (ข้ามไฟล์ที่มีอยู่แล้ว)
6
+ * npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ข้ามไฟล์ที่มีอยู่แล้ว)
7
+ * npx @warnyin/agents --global ติดตั้งแบบ global (~/) ใช้ได้ทุกโปรเจกต์
8
+ * npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ — ไม่ถาม)
7
9
  * npx @warnyin/agents --update อัปเดต playbook กลาง (เขียนทับเฉพาะไฟล์ core)
8
10
  * npx @warnyin/agents --dry-run แสดงว่าจะทำอะไร โดยไม่เขียนไฟล์จริง
9
11
  * (ทางสำรองไม่ผ่าน npm: npx github:warnyin/warnyin-agents)
10
12
  */
11
13
  import fs from 'node:fs'
14
+ import os from 'node:os'
12
15
  import path from 'node:path'
13
16
  import { fileURLToPath } from 'node:url'
17
+ import { createInterface } from 'node:readline/promises'
14
18
 
15
19
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
16
- const target = process.cwd()
20
+ const cwd = process.cwd()
17
21
  const args = new Set(process.argv.slice(2))
18
22
  const UPDATE = args.has('--update')
19
23
  const DRY = args.has('--dry-run')
20
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
+
21
41
  if (args.has('--help') || args.has('-h')) {
22
- console.log(`warnyin-agents — ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน
42
+ console.log(`warnyin-agents — ติดตั้ง Warnyin Standard Workflow ลงโปรเจกต์ปัจจุบัน หรือแบบ global
23
43
 
24
44
  ใช้งาน:
25
- npx @warnyin/agents ติดตั้ง (ข้ามไฟล์ที่มีอยู่แล้ว ไม่เขียนทับ)
45
+ npx @warnyin/agents ติดตั้งลงโปรเจกต์ (ถ้า TTY จะถามก่อน; ข้ามไฟล์ที่มีอยู่แล้ว)
46
+ npx @warnyin/agents --global ติดตั้งแบบ global ลง ~/ (~/.warnyin + ~/.claude) ใช้ได้ทุกโปรเจกต์
47
+ npx @warnyin/agents --project ติดตั้งลงโปรเจกต์ (บังคับ ไม่ถาม)
26
48
  npx @warnyin/agents --update อัปเดต playbook กลางเป็นเวอร์ชันล่าสุด
27
49
  (เขียนทับเฉพาะ .warnyin/workflow/, .claude/commands/warnyin/,
28
50
  template .warnyin/template/stages/[topic] — ไม่แตะ docs/ และงานจริง)
@@ -33,15 +55,15 @@ if (args.has('--help') || args.has('-h')) {
33
55
  }
34
56
 
35
57
  // guard กัน self-install — เก็บไว้แบบ defensive (zero-cost)
36
- // หลังย้าย source เข้า src/ → pkgRoot = src/ (sibling ของ bin/) จึงแทบไม่มีทาง === target (repo root / temp sandbox)
58
+ // หลังย้าย source เข้า src/ → pkgRoot = src/ (sibling ของ bin/) จึงแทบไม่มีทาง === cwd (repo root / temp sandbox)
37
59
  // → guard นี้เป็น no-op โดยตั้งใจในเคสปกติ/sandbox; ยังคงดักได้เฉพาะ edge case ที่ install ลงโฟลเดอร์ที่เป็น src/ เอง
38
- if (path.resolve(pkgRoot) === path.resolve(target)) {
60
+ if (path.resolve(pkgRoot) === path.resolve(cwd)) {
39
61
  console.error('✖ กำลังรันอยู่ใน repo ของ warnyin-agents เอง — ให้ cd ไปที่โปรเจกต์ปลายทางก่อน')
40
62
  process.exit(1)
41
63
  }
42
64
 
43
65
  // โครงเก่า (≤0.2.x): workflow/ + warnyin-stages/ ที่ root — เตือนให้ย้ายเอง ไม่แตะงานจริงของ user
44
- const legacyV2 = ['workflow', 'warnyin-stages'].filter((d) => fs.existsSync(path.join(target, d)))
66
+ const legacyV2 = ['workflow', 'warnyin-stages'].filter((d) => fs.existsSync(path.join(cwd, d)))
45
67
  if (legacyV2.length) {
46
68
  console.warn(`⚠ พบโครงเลย์เอาต์เก่า (≤0.2.x): ${legacyV2.join(', ')}
47
69
  เวอร์ชันนี้ย้าย core ไปใต้ .warnyin/ และงานจริงไป docs/stages/ — แนะนำย้ายด้วยตัวเองก่อน:
@@ -52,7 +74,7 @@ if (legacyV2.length) {
52
74
 
53
75
  // โครงเก่า (0.3–0.5.x): ทุกอย่างอยู่ใต้ warnyin/ ที่ root — เวอร์ชันนี้แยกเป็น .warnyin/ (core) + docs/stages (งานจริง)
54
76
  const legacyV5 = ['workflow', 'template', 'installer', 'stages'].filter((d) =>
55
- fs.existsSync(path.join(target, 'warnyin', d)),
77
+ fs.existsSync(path.join(cwd, 'warnyin', d)),
56
78
  )
57
79
  if (legacyV5.length) {
58
80
  console.warn(`⚠ พบโครงเลย์เอาต์เก่า (0.3–0.5.x): warnyin/{${legacyV5.join(', ')}}
@@ -79,6 +101,9 @@ const SCAFFOLD_FILES = [
79
101
 
80
102
  const stats = { created: 0, updated: 0, skipped: 0 }
81
103
 
104
+ // target = ปลายทางที่จะเขียนไฟล์ — ตั้งหลัง resolve mode (project=cwd | global=homedir)
105
+ let target = cwd
106
+
82
107
  function copyTree(relDir, { overwrite }) {
83
108
  const srcDir = path.join(pkgRoot, relDir)
84
109
  if (!fs.existsSync(srcDir)) return
@@ -173,21 +198,113 @@ function installRootDoc(name, srcPath) {
173
198
  console.log(` ± ${name} (ต่อท้าย section Warnyin Standard Workflow)`)
174
199
  }
175
200
 
176
- console.log(`Warnyin Standard Workflow → ${target}${DRY ? ' (dry-run)' : ''}\n`)
201
+ const GLOBAL_NOTE_MARKER = '<!-- warnyin:global-note -->'
177
202
 
178
- for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
179
- ensureScaffold()
180
- seedDocs()
181
- installRootDoc('CLAUDE.md', path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.md'))
182
- installRootDoc('AGENTS.md', path.join(pkgRoot, 'AGENTS.md'))
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}`)
183
288
 
184
- console.log(`\nสรุป: สร้างใหม่ ${stats.created} · อัปเดต ${stats.updated} · ข้าม (มีอยู่แล้ว) ${stats.skipped}`)
289
+ if (!UPDATE) {
290
+ if (mode === 'global') {
291
+ console.log(`
292
+ ติดตั้ง global แล้ว — /warnyin:* ใช้ได้ทุกโปรเจกต์ (จาก ~/.claude/commands/)
293
+ เปิดโปรเจกต์ใด ๆ ใน Claude Code → รัน /warnyin:init เพื่อสร้าง workspace (docs/) ของโปรเจกต์นั้น
185
294
 
186
- if (!UPDATE) {
187
- console.log(`
295
+ อัปเดต playbook ภายหลัง: npx @warnyin/agents --global --update`)
296
+ } else {
297
+ console.log(`
188
298
  ขั้นถัดไป:
189
299
  1. เปิด Claude Code ในโปรเจกต์นี้ แล้วรัน /warnyin:init — ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/
190
300
  2. เริ่มงานแรก: /warnyin:discovery <topic> หรือ /warnyin:design <slug> <change>
191
301
 
192
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()
193
310
  }