@warnyin/agents 0.18.0 → 0.19.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +173 -162
  2. package/README.md +160 -160
  3. package/package.json +38 -38
  4. package/src/.claude/agents/warnyin-infra.md +13 -13
  5. package/src/.claude/agents/warnyin-qa.md +13 -13
  6. package/src/.claude/agents/warnyin-sa.md +13 -13
  7. package/src/.claude/agents/warnyin-security.md +13 -13
  8. package/src/.claude/agents/warnyin-tech-lead.md +13 -13
  9. package/src/.claude/agents/warnyin-ux.md +14 -14
  10. package/src/.claude/commands/warnyin/build.md +31 -31
  11. package/src/.claude/commands/warnyin/design.md +27 -27
  12. package/src/.claude/commands/warnyin/discovery.md +22 -22
  13. package/src/.claude/commands/warnyin/explore.md +14 -14
  14. package/src/.claude/commands/warnyin/feedback/issue.md +14 -14
  15. package/src/.claude/commands/warnyin/init.md +12 -12
  16. package/src/.claude/commands/warnyin/install-skill.md +19 -19
  17. package/src/.claude/commands/warnyin/next.md +17 -17
  18. package/src/.claude/commands/warnyin/ship.md +28 -28
  19. package/src/.claude/commands/warnyin/triage.md +14 -14
  20. package/src/.claude/commands/warnyin/update-codemaps.md +12 -12
  21. package/src/.claude/commands/warnyin/verify.md +20 -20
  22. package/src/.claude/skills/explore/SKILL.md +8 -8
  23. package/src/.claude/skills/next/SKILL.md +8 -8
  24. package/src/.claude/skills/update-codemaps/SKILL.md +8 -8
  25. package/src/.warnyin/installer/templates/CLAUDE.global.md +5 -5
  26. package/src/.warnyin/installer/templates/CLAUDE.md +35 -35
  27. package/src/.warnyin/template/docs/codemap/index.md +18 -18
  28. package/src/.warnyin/template/docs/features/[feature-name]/business.md +5 -5
  29. package/src/.warnyin/template/docs/features/[feature-name]/feature.md +5 -5
  30. package/src/.warnyin/template/docs/features/[feature-name]/spec.md +16 -16
  31. package/src/.warnyin/template/docs/infra.md +16 -16
  32. package/src/.warnyin/template/docs/project.md +18 -18
  33. package/src/.warnyin/template/docs/rule.md +7 -7
  34. package/src/.warnyin/template/docs/techstack/[component]/about.md +6 -6
  35. package/src/.warnyin/template/docs/techstack/[component]/rule.md +6 -6
  36. package/src/.warnyin/template/docs/techstack/[component]/standard.md +6 -6
  37. package/src/.warnyin/template/docs/techstack/[component]/structure.md +7 -7
  38. package/src/.warnyin/template/docs/techstack/[component]/test.md +7 -7
  39. package/src/.warnyin/template/docs/troubleshooting.md +32 -32
  40. package/src/.warnyin/template/stages/[topic]/build.md +58 -58
  41. package/src/.warnyin/template/stages/[topic]/business.md +21 -21
  42. package/src/.warnyin/template/stages/[topic]/design.md +63 -63
  43. package/src/.warnyin/template/stages/[topic]/discovery.md +69 -69
  44. package/src/.warnyin/template/stages/[topic]/proposal.md +43 -43
  45. package/src/.warnyin/template/stages/[topic]/research.md +49 -49
  46. package/src/.warnyin/template/stages/[topic]/ship.md +32 -32
  47. package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/issue.md +19 -19
  48. package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/rule.md +13 -13
  49. package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/spec.md +36 -36
  50. package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/standard.md +21 -21
  51. package/src/.warnyin/template/stages/[topic]/tasks/[task-name]/task.md +40 -40
  52. package/src/.warnyin/template/stages/[topic]/test.md +46 -46
  53. package/src/.warnyin/template/stages/[topic]/troubleshooting.md +34 -34
  54. package/src/.warnyin/template/stages/[topic]/verify.md +44 -44
  55. package/src/.warnyin/template/stages/[topic]/wireframe.md +104 -104
  56. package/src/.warnyin/workflow/README.md +107 -106
  57. package/src/.warnyin/workflow/api-doc.md +93 -93
  58. package/src/.warnyin/workflow/codemap.md +91 -91
  59. package/src/.warnyin/workflow/contexts/README.md +51 -51
  60. package/src/.warnyin/workflow/contexts/build.md +26 -25
  61. package/src/.warnyin/workflow/contexts/research.md +25 -25
  62. package/src/.warnyin/workflow/contexts/review.md +29 -25
  63. package/src/.warnyin/workflow/explore.md +32 -32
  64. package/src/.warnyin/workflow/feedback.md +212 -212
  65. package/src/.warnyin/workflow/init.md +136 -136
  66. package/src/.warnyin/workflow/minimalism.md +63 -0
  67. package/src/.warnyin/workflow/next.md +48 -48
  68. package/src/.warnyin/workflow/roles/README.md +52 -52
  69. package/src/.warnyin/workflow/roles/ba.md +25 -25
  70. package/src/.warnyin/workflow/roles/developer.md +32 -31
  71. package/src/.warnyin/workflow/roles/infra.md +24 -24
  72. package/src/.warnyin/workflow/roles/po.md +28 -28
  73. package/src/.warnyin/workflow/roles/qa.md +36 -36
  74. package/src/.warnyin/workflow/roles/sa.md +28 -28
  75. package/src/.warnyin/workflow/roles/security.md +39 -39
  76. package/src/.warnyin/workflow/roles/tech-lead.md +28 -28
  77. package/src/.warnyin/workflow/roles/ux.md +76 -76
  78. package/src/.warnyin/workflow/scripts/build-wave.mjs +145 -145
  79. package/src/.warnyin/workflow/scripts/validate-topic.mjs +378 -378
  80. package/src/.warnyin/workflow/stages/build.md +98 -98
  81. package/src/.warnyin/workflow/stages/design.md +174 -174
  82. package/src/.warnyin/workflow/stages/discovery.md +256 -256
  83. package/src/.warnyin/workflow/stages/ship.md +94 -94
  84. package/src/.warnyin/workflow/stages/verify.md +82 -82
  85. package/src/.warnyin/workflow/triage.md +74 -74
  86. package/src/AGENTS.md +54 -54
  87. package/src/bin/cli.mjs +357 -333
package/src/bin/cli.mjs CHANGED
@@ -1,333 +1,357 @@
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
- /** อ่าน version ของ package ที่กำลังติดตั้ง — ผลลัพธ์: version string (เช่น "0.16.0") */
202
- function readPkgVersion() {
203
- return JSON.parse(fs.readFileSync(path.join(pkgRoot, '..', 'package.json'), 'utf8')).version
204
- }
205
-
206
- /** เขียน version stamp ลง <target>/.warnyin/.warnyin-version — unconditional overwrite (ไม่ skip-if-equal)
207
- * เคารพ DRY (ไม่เขียนจริงตอน dry-run แต่ log + นับ stats)
208
- */
209
- function writeVersionStamp() {
210
- const ver = readPkgVersion()
211
- const rel = path.join('.warnyin', '.warnyin-version')
212
- const dest = path.join(target, rel)
213
- const exists = fs.existsSync(dest)
214
- if (!DRY) {
215
- fs.mkdirSync(path.dirname(dest), { recursive: true })
216
- fs.writeFileSync(dest, ver + '\n') // ★ unconditional — ไม่ byte-equal skip แบบ copyTree
217
- }
218
- stats[exists ? 'updated' : 'created']++
219
- console.log(` ${exists ? '↻' : '+'} ${rel}`)
220
- }
221
-
222
- const GLOBAL_NOTE_MARKER = '<!-- warnyin:global-note -->'
223
-
224
- /**
225
- * เขียน resolution note ลง ~/.claude/CLAUDE.md แบบ note-only append-with-marker (global mode)
226
- * — ห้ามเขียนทับทั้งไฟล์ (personal global memory ของ user); append เฉพาะถ้ายังไม่มี marker (idempotent)
227
- * — defensive skip ถ้า template ไม่มี (worktree T1 เดี่ยวก่อน merge T2) — pattern เดียวกับ copyTree/seedDocs
228
- */
229
- function installGlobalNote() {
230
- const src = path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.global.md')
231
- const destRel = path.join('.claude', 'CLAUDE.md')
232
- const dest = path.join(target, destRel)
233
- if (!fs.existsSync(src)) {
234
- console.log(` · ข้าม ${destRel} (ยังไม่มี template CLAUDE.global.md)`)
235
- return
236
- }
237
- const note = fs.readFileSync(src, 'utf8')
238
- if (!fs.existsSync(dest)) {
239
- if (!DRY) {
240
- fs.mkdirSync(path.dirname(dest), { recursive: true })
241
- fs.writeFileSync(dest, note)
242
- }
243
- stats.created++
244
- console.log(` + ${destRel}`)
245
- return
246
- }
247
- const existing = fs.readFileSync(dest, 'utf8')
248
- if (existing.includes(GLOBAL_NOTE_MARKER)) {
249
- stats.skipped++
250
- return
251
- }
252
- const section = (existing.endsWith('\n') ? '\n' : '\n\n') + note
253
- if (!DRY) fs.appendFileSync(dest, section)
254
- stats.updated++
255
- console.log(` ± ${destRel} (ต่อท้าย warnyin global note)`)
256
- }
257
-
258
- /** เลือก mode + ติดตั้งตาม mode (target=cwd|homedir). ห่อ async เฉพาะ path TTY (readline) */
259
- async function main() {
260
- const globalFlag = args.has('--global')
261
- const projectFlag = args.has('--project')
262
- const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY)
263
-
264
- let mode
265
- try {
266
- if (!globalFlag && !projectFlag && isTTY) {
267
- // ถาม เฉพาะ path TTY — non-TTY/flag ไม่แตะ readline (ไม่ค้าง/ไม่ช้าลง)
268
- const rl = createInterface({ input: process.stdin, output: process.stdout })
269
- let answer
270
- try {
271
- answer = await rl.question('ติดตั้งแบบไหน? [1] โปรเจกต์นี้ (default) [2] global (~/) : ')
272
- } finally {
273
- rl.close()
274
- }
275
- mode = resolveMode({ globalFlag, projectFlag, isTTY, answer })
276
- } else {
277
- mode = resolveMode({ globalFlag, projectFlag, isTTY })
278
- }
279
- } catch (e) {
280
- console.error(`✖ ${e.message}`)
281
- process.exit(1)
282
- }
283
-
284
- if (mode === 'global') {
285
- const home = os.homedir()
286
- // homedir guard — falsy หรือ filesystem root (CI/container ไม่มี passwd) → error แทนเขียนลง root
287
- if (!home || path.resolve(home) === path.parse(path.resolve(home)).root) {
288
- console.error('✖ หา home directory ไม่ได้ (หรือเป็น filesystem root) — ใช้ --project แทน')
289
- process.exit(1)
290
- }
291
- target = home
292
- console.log(`Warnyin Standard Workflow → global ${target}${DRY ? ' (dry-run)' : ''}`)
293
- console.log(` จะเขียนนอกโปรเจกต์ที่: ${path.join(target, '.warnyin')}, ${path.join(target, '.claude', 'commands', 'warnyin')}, ${path.join(target, '.claude', 'CLAUDE.md')}\n`)
294
- // first-install overwrite:false (skip ของเดิมใน ~/.claude/{agents,skills}) — ทับเฉพาะ --global --update
295
- for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
296
- writeVersionStamp()
297
- // ข้าม scaffold/seedDocs (ยกให้ /warnyin:init) + ข้าม AGENTS.md global (DQ3 limitation)
298
- installGlobalNote()
299
- } else {
300
- target = cwd
301
- console.log(`Warnyin Standard Workflow → ${target}${DRY ? ' (dry-run)' : ''}\n`)
302
- for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
303
- writeVersionStamp()
304
- ensureScaffold()
305
- seedDocs()
306
- installRootDoc('CLAUDE.md', path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.md'))
307
- installRootDoc('AGENTS.md', path.join(pkgRoot, 'AGENTS.md'))
308
- }
309
-
310
- console.log(`\nสรุป: สร้างใหม่ ${stats.created} · อัปเดต ${stats.updated} · ข้าม (มีอยู่แล้ว) ${stats.skipped}`)
311
-
312
- if (!UPDATE) {
313
- if (mode === 'global') {
314
- console.log(`
315
- ติดตั้ง global แล้ว — /warnyin:* ใช้ได้ทุกโปรเจกต์ (จาก ~/.claude/commands/)
316
- เปิดโปรเจกต์ใด ๆ ใน Claude Code → รัน /warnyin:init เพื่อสร้าง workspace (docs/) ของโปรเจกต์นั้น
317
-
318
- อัปเดต playbook ภายหลัง: npx @warnyin/agents --global --update`)
319
- } else {
320
- console.log(`
321
- ขั้นถัดไป:
322
- 1. เปิด Claude Code ในโปรเจกต์นี้ แล้วรัน /warnyin:init — ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/
323
- 2. เริ่มงานแรก: /warnyin:discovery <topic> หรือ /warnyin:design <slug> <change>
324
-
325
- อัปเดต playbook ภายหลัง: npx @warnyin/agents --update`)
326
- }
327
- }
328
- }
329
-
330
- // main-guard: รันเฉพาะตอน execute ตรง ๆ (ไม่ trigger ตอน import เพื่อ unit-test resolveMode)
331
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
332
- await main()
333
- }
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
+ /** อ่าน version ของ package ที่กำลังติดตั้ง — ผลลัพธ์: version string (เช่น "0.16.0") */
202
+ function readPkgVersion() {
203
+ return JSON.parse(fs.readFileSync(path.join(pkgRoot, '..', 'package.json'), 'utf8')).version
204
+ }
205
+
206
+ /** เขียน version stamp ลง <target>/.warnyin/.warnyin-version — unconditional overwrite (ไม่ skip-if-equal)
207
+ * เคารพ DRY (ไม่เขียนจริงตอน dry-run แต่ log + นับ stats)
208
+ */
209
+ function writeVersionStamp() {
210
+ const ver = readPkgVersion()
211
+ const rel = path.join('.warnyin', '.warnyin-version')
212
+ const dest = path.join(target, rel)
213
+ const exists = fs.existsSync(dest)
214
+ if (!DRY) {
215
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
216
+ fs.writeFileSync(dest, ver + '\n') // ★ unconditional — ไม่ byte-equal skip แบบ copyTree
217
+ }
218
+ stats[exists ? 'updated' : 'created']++
219
+ console.log(` ${exists ? '↻' : '+'} ${rel}`)
220
+ }
221
+
222
+ const GLOBAL_NOTE_MARKER = '<!-- warnyin:global-note -->'
223
+
224
+ /**
225
+ * เขียน resolution note ลง ~/.claude/CLAUDE.md แบบ note-only append-with-marker (global mode)
226
+ * — ห้ามเขียนทับทั้งไฟล์ (personal global memory ของ user); append เฉพาะถ้ายังไม่มี marker (idempotent)
227
+ * — defensive skip ถ้า template ไม่มี (worktree T1 เดี่ยวก่อน merge T2) — pattern เดียวกับ copyTree/seedDocs
228
+ */
229
+ function installGlobalNote() {
230
+ const src = path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.global.md')
231
+ const destRel = path.join('.claude', 'CLAUDE.md')
232
+ const dest = path.join(target, destRel)
233
+ if (!fs.existsSync(src)) {
234
+ console.log(` · ข้าม ${destRel} (ยังไม่มี template CLAUDE.global.md)`)
235
+ return
236
+ }
237
+ const note = fs.readFileSync(src, 'utf8')
238
+ if (!fs.existsSync(dest)) {
239
+ if (!DRY) {
240
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
241
+ fs.writeFileSync(dest, note)
242
+ }
243
+ stats.created++
244
+ console.log(` + ${destRel}`)
245
+ return
246
+ }
247
+ const existing = fs.readFileSync(dest, 'utf8')
248
+ if (existing.includes(GLOBAL_NOTE_MARKER)) {
249
+ stats.skipped++
250
+ return
251
+ }
252
+ const section = (existing.endsWith('\n') ? '\n' : '\n\n') + note
253
+ if (!DRY) fs.appendFileSync(dest, section)
254
+ stats.updated++
255
+ console.log(` ± ${destRel} (ต่อท้าย warnyin global note)`)
256
+ }
257
+
258
+ /** เลือก mode + ติดตั้งตาม mode (target=cwd|homedir). ห่อ async เฉพาะ path TTY (readline) */
259
+ async function main() {
260
+ const globalFlag = args.has('--global')
261
+ const projectFlag = args.has('--project')
262
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY)
263
+
264
+ let mode
265
+ try {
266
+ if (!globalFlag && !projectFlag && isTTY) {
267
+ // ถาม เฉพาะ path TTY — non-TTY/flag ไม่แตะ readline (ไม่ค้าง/ไม่ช้าลง)
268
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
269
+ let answer
270
+ try {
271
+ answer = await rl.question('ติดตั้งแบบไหน? [1] โปรเจกต์นี้ (default) [2] global (~/) : ')
272
+ } finally {
273
+ rl.close()
274
+ }
275
+ mode = resolveMode({ globalFlag, projectFlag, isTTY, answer })
276
+ } else {
277
+ mode = resolveMode({ globalFlag, projectFlag, isTTY })
278
+ }
279
+ } catch (e) {
280
+ console.error(`✖ ${e.message}`)
281
+ process.exit(1)
282
+ }
283
+
284
+ if (mode === 'global') {
285
+ const home = os.homedir()
286
+ // homedir guard — falsy หรือ filesystem root (CI/container ไม่มี passwd) → error แทนเขียนลง root
287
+ if (!home || path.resolve(home) === path.parse(path.resolve(home)).root) {
288
+ console.error('✖ หา home directory ไม่ได้ (หรือเป็น filesystem root) — ใช้ --project แทน')
289
+ process.exit(1)
290
+ }
291
+ target = home
292
+ console.log(`Warnyin Standard Workflow → global ${target}${DRY ? ' (dry-run)' : ''}`)
293
+ console.log(` จะเขียนนอกโปรเจกต์ที่: ${path.join(target, '.warnyin')}, ${path.join(target, '.claude', 'commands', 'warnyin')}, ${path.join(target, '.claude', 'CLAUDE.md')}\n`)
294
+ // first-install overwrite:false (skip ของเดิมใน ~/.claude/{agents,skills}) — ทับเฉพาะ --global --update
295
+ for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
296
+ writeVersionStamp()
297
+ // ข้าม scaffold/seedDocs (ยกให้ /warnyin:init) + ข้าม AGENTS.md global (DQ3 limitation)
298
+ installGlobalNote()
299
+ } else {
300
+ target = cwd
301
+ console.log(`Warnyin Standard Workflow → ${target}${DRY ? ' (dry-run)' : ''}\n`)
302
+ for (const dir of CORE) copyTree(dir, { overwrite: UPDATE })
303
+ writeVersionStamp()
304
+ ensureScaffold()
305
+ seedDocs()
306
+ installRootDoc('CLAUDE.md', path.join(pkgRoot, '.warnyin', 'installer', 'templates', 'CLAUDE.md'))
307
+ installRootDoc('AGENTS.md', path.join(pkgRoot, 'AGENTS.md'))
308
+ }
309
+
310
+ console.log(`\nสรุป: สร้างใหม่ ${stats.created} · อัปเดต ${stats.updated} · ข้าม (มีอยู่แล้ว) ${stats.skipped}`)
311
+
312
+ if (!UPDATE) {
313
+ if (mode === 'global') {
314
+ console.log(`
315
+ ติดตั้ง global แล้ว — /warnyin:* ใช้ได้ทุกโปรเจกต์ (จาก ~/.claude/commands/)
316
+ เปิดโปรเจกต์ใด ๆ ใน Claude Code → รัน /warnyin:init เพื่อสร้าง workspace (docs/) ของโปรเจกต์นั้น
317
+
318
+ อัปเดต playbook ภายหลัง: npx @warnyin/agents --global --update`)
319
+ } else {
320
+ console.log(`
321
+ ขั้นถัดไป:
322
+ 1. เปิด Claude Code ในโปรเจกต์นี้ แล้วรัน /warnyin:init — ให้ agent วิเคราะห์โปรเจกต์ + เติม docs/
323
+ 2. เริ่มงานแรก: /warnyin:discovery <topic> หรือ /warnyin:design <slug> <change>
324
+
325
+ อัปเดต playbook ภายหลัง: npx @warnyin/agents --update`)
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * ตรวจว่า module ถูก execute ตรง ๆ (entrypoint) ไม่ใช่ถูก import — เทียบ argv[1] กับ path ของ module เอง
332
+ * ★ ต้อง realpath argv[1] ด้วย: ESM `import.meta.url` เป็น realpath (resolve symlink) เสมอ แต่ argv[1]
333
+ * เป็น path ตามที่ผู้เรียกระบุ (ไม่ resolve symlink). เมื่อรันผ่าน symlink — npx รัน bin ผ่าน
334
+ * `.bin/<name>` symlink, setup:dogfood extract tarball ลง `os.tmpdir()` ที่เป็น symlink บน macOS
335
+ * (`/var/folders/.../T` → `/private/var/...`) — `path.resolve(argv[1])` จะคืน symlink path ≠ realpath
336
+ * ของ module → mismatch → main() ไม่ถูกเรียก → installer เงียบ exit 0 ไม่ทำอะไร (bug critical:
337
+ * กระทบทั้ง npx ของผู้ใช้ปลายทาง + setup:dogfood). realpath ทั้งสองฝั่งจึง match ถูกต้อง.
338
+ * @param {string|undefined} argv1 — process.argv[1] (path ของ script ที่ node รัน)
339
+ * @param {string} metaUrl — import.meta.url ของ module (เป็น realpath เสมอใน ESM)
340
+ * @param {(p: string) => string} [realpath] — injectable เพื่อ unit-test (default fs.realpathSync)
341
+ * @returns {boolean}
342
+ */
343
+ export function isEntrypoint(argv1, metaUrl, realpath = fs.realpathSync) {
344
+ if (!argv1) return false
345
+ const self = fileURLToPath(metaUrl)
346
+ try {
347
+ return realpath(argv1) === self
348
+ } catch {
349
+ // argv1 resolve เป็น realpath ไม่ได้ (ไฟล์ไม่อยู่จริง ฯลฯ) → fallback เทียบ path ปกติ
350
+ return path.resolve(argv1) === self
351
+ }
352
+ }
353
+
354
+ // main-guard: รันเฉพาะตอน execute ตรง ๆ (ไม่ trigger ตอน import เพื่อ unit-test resolveMode/isEntrypoint)
355
+ if (isEntrypoint(process.argv[1], import.meta.url)) {
356
+ await main()
357
+ }