dw-kit 1.0.1 → 1.0.2
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/.claude/rules/code-style.md +37 -37
- package/.claude/rules/commit-standards.md +37 -37
- package/.claude/settings.json +1 -1
- package/.claude/settings.local.json +2 -1
- package/.claude/skills/dw-prompt/SKILL.md +62 -0
- package/CLAUDE.md +1 -0
- package/README.md +27 -12
- package/package.json +2 -1
- package/src/__fixtures__/claude-cli-bug-snippet.js +15 -0
- package/src/cli.mjs +33 -0
- package/src/commands/claude-vn-fix.mjs +267 -0
- package/src/commands/prompt.mjs +125 -0
- package/src/lib/clipboard.mjs +24 -0
- package/src/lib/prompt-suggest.mjs +84 -0
- package/src/lib/update-checker.mjs +73 -0
- package/src/smoke-test.mjs +47 -1
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Code Style & Conventions
|
|
2
|
-
|
|
3
|
-
## Nguyên tắc chung
|
|
4
|
-
- Đặt tên biến/hàm rõ ràng, tự giải thích (self-documenting)
|
|
5
|
-
- Ưu tiên đơn giản, dễ đọc hơn clever code
|
|
6
|
-
- Mỗi function làm MỘT việc
|
|
7
|
-
- Comments giải thích WHY, không phải WHAT
|
|
8
|
-
- Xử lý errors ở đầu function (guard clauses / early return)
|
|
9
|
-
|
|
10
|
-
## Naming Conventions
|
|
11
|
-
- Variables/Functions: camelCase
|
|
12
|
-
- Classes/Components: PascalCase
|
|
13
|
-
- Constants: UPPER_SNAKE_CASE
|
|
14
|
-
- Files: kebab-case hoặc theo convention của framework
|
|
15
|
-
- Directories: kebab-case
|
|
16
|
-
|
|
17
|
-
## File Organization
|
|
18
|
-
- 1 component/class per file (trừ khi strongly related)
|
|
19
|
-
- Group imports: external → internal → relative
|
|
20
|
-
- Export ở cuối file hoặc inline (nhất quán trong project)
|
|
21
|
-
|
|
22
|
-
## Error Handling
|
|
23
|
-
- KHÔNG swallow errors (catch rỗng)
|
|
24
|
-
- Log đủ context để debug (error message, input data, stack)
|
|
25
|
-
- Dùng custom error types cho domain errors
|
|
26
|
-
- Validate input ở boundary (API, form, external data)
|
|
27
|
-
|
|
28
|
-
## Testing
|
|
29
|
-
- Test file cùng tên với source: `foo.ts` → `foo.test.ts` hoặc `foo.spec.ts`
|
|
30
|
-
- Mỗi test case kiểm tra MỘT behavior
|
|
31
|
-
- Test name mô tả expected behavior: "should return error when input is empty"
|
|
32
|
-
- Arrange → Act → Assert pattern
|
|
33
|
-
- KHÔNG mock internal implementation details
|
|
34
|
-
|
|
35
|
-
## NOTE
|
|
36
|
-
Đây là quy tắc mặc định. Team tùy chỉnh theo stack cụ thể của dự án.
|
|
37
|
-
Thêm framework-specific rules vào file này hoặc tạo file riêng trong `.claude/rules/`.
|
|
1
|
+
# Code Style & Conventions
|
|
2
|
+
|
|
3
|
+
## Nguyên tắc chung
|
|
4
|
+
- Đặt tên biến/hàm rõ ràng, tự giải thích (self-documenting)
|
|
5
|
+
- Ưu tiên đơn giản, dễ đọc hơn clever code
|
|
6
|
+
- Mỗi function làm MỘT việc
|
|
7
|
+
- Comments giải thích WHY, không phải WHAT
|
|
8
|
+
- Xử lý errors ở đầu function (guard clauses / early return)
|
|
9
|
+
|
|
10
|
+
## Naming Conventions
|
|
11
|
+
- Variables/Functions: camelCase
|
|
12
|
+
- Classes/Components: PascalCase
|
|
13
|
+
- Constants: UPPER_SNAKE_CASE
|
|
14
|
+
- Files: kebab-case hoặc theo convention của framework
|
|
15
|
+
- Directories: kebab-case
|
|
16
|
+
|
|
17
|
+
## File Organization
|
|
18
|
+
- 1 component/class per file (trừ khi strongly related)
|
|
19
|
+
- Group imports: external → internal → relative
|
|
20
|
+
- Export ở cuối file hoặc inline (nhất quán trong project)
|
|
21
|
+
|
|
22
|
+
## Error Handling
|
|
23
|
+
- KHÔNG swallow errors (catch rỗng)
|
|
24
|
+
- Log đủ context để debug (error message, input data, stack)
|
|
25
|
+
- Dùng custom error types cho domain errors
|
|
26
|
+
- Validate input ở boundary (API, form, external data)
|
|
27
|
+
|
|
28
|
+
## Testing
|
|
29
|
+
- Test file cùng tên với source: `foo.ts` → `foo.test.ts` hoặc `foo.spec.ts`
|
|
30
|
+
- Mỗi test case kiểm tra MỘT behavior
|
|
31
|
+
- Test name mô tả expected behavior: "should return error when input is empty"
|
|
32
|
+
- Arrange → Act → Assert pattern
|
|
33
|
+
- KHÔNG mock internal implementation details
|
|
34
|
+
|
|
35
|
+
## NOTE
|
|
36
|
+
Đây là quy tắc mặc định. Team tùy chỉnh theo stack cụ thể của dự án.
|
|
37
|
+
Thêm framework-specific rules vào file này hoặc tạo file riêng trong `.claude/rules/`.
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Commit Standards
|
|
2
|
-
|
|
3
|
-
## Format
|
|
4
|
-
```
|
|
5
|
-
<type>(<scope>): <mô tả tiếng Việt hoặc tiếng Anh>
|
|
6
|
-
|
|
7
|
-
[Body - chi tiết thay đổi, lý do]
|
|
8
|
-
[Blank line]
|
|
9
|
-
[Footer - breaking changes, references]
|
|
10
|
-
|
|
11
|
-
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Types
|
|
15
|
-
| Type | Khi nào dùng |
|
|
16
|
-
|------|-------------|
|
|
17
|
-
| `feat` | Tính năng mới |
|
|
18
|
-
| `fix` | Sửa lỗi |
|
|
19
|
-
| `refactor` | Tái cấu trúc, không thay đổi behavior |
|
|
20
|
-
| `test` | Thêm/sửa tests |
|
|
21
|
-
| `docs` | Tài liệu, comments |
|
|
22
|
-
| `chore` | Build, config, dependencies |
|
|
23
|
-
| `style` | Format, whitespace (không thay đổi logic) |
|
|
24
|
-
| `perf` | Cải thiện performance |
|
|
25
|
-
|
|
26
|
-
## Quy tắc
|
|
27
|
-
- Mỗi commit = 1 subtask hoặc 1 đơn vị logic hoàn chỉnh
|
|
28
|
-
- Mô tả ngắn <= 72 ký tự
|
|
29
|
-
- Dùng thì hiện tại: "thêm", "sửa", "cập nhật" (không phải "đã thêm")
|
|
30
|
-
- KHÔNG commit files chứa secrets (.env, credentials, tokens)
|
|
31
|
-
- KHÔNG commit console.log/debugger còn sót
|
|
32
|
-
|
|
33
|
-
## Branch Naming
|
|
34
|
-
```
|
|
35
|
-
<type>/<task-name>
|
|
36
|
-
```
|
|
37
|
-
Ví dụ: `feat/user-auth`, `fix/login-redirect`, `refactor/api-structure`
|
|
1
|
+
# Commit Standards
|
|
2
|
+
|
|
3
|
+
## Format
|
|
4
|
+
```
|
|
5
|
+
<type>(<scope>): <mô tả tiếng Việt hoặc tiếng Anh>
|
|
6
|
+
|
|
7
|
+
[Body - chi tiết thay đổi, lý do]
|
|
8
|
+
[Blank line]
|
|
9
|
+
[Footer - breaking changes, references]
|
|
10
|
+
|
|
11
|
+
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Types
|
|
15
|
+
| Type | Khi nào dùng |
|
|
16
|
+
|------|-------------|
|
|
17
|
+
| `feat` | Tính năng mới |
|
|
18
|
+
| `fix` | Sửa lỗi |
|
|
19
|
+
| `refactor` | Tái cấu trúc, không thay đổi behavior |
|
|
20
|
+
| `test` | Thêm/sửa tests |
|
|
21
|
+
| `docs` | Tài liệu, comments |
|
|
22
|
+
| `chore` | Build, config, dependencies |
|
|
23
|
+
| `style` | Format, whitespace (không thay đổi logic) |
|
|
24
|
+
| `perf` | Cải thiện performance |
|
|
25
|
+
|
|
26
|
+
## Quy tắc
|
|
27
|
+
- Mỗi commit = 1 subtask hoặc 1 đơn vị logic hoàn chỉnh
|
|
28
|
+
- Mô tả ngắn <= 72 ký tự
|
|
29
|
+
- Dùng thì hiện tại: "thêm", "sửa", "cập nhật" (không phải "đã thêm")
|
|
30
|
+
- KHÔNG commit files chứa secrets (.env, credentials, tokens)
|
|
31
|
+
- KHÔNG commit console.log/debugger còn sót
|
|
32
|
+
|
|
33
|
+
## Branch Naming
|
|
34
|
+
```
|
|
35
|
+
<type>/<task-name>
|
|
36
|
+
```
|
|
37
|
+
Ví dụ: `feat/user-auth`, `fix/login-redirect`, `refactor/api-structure`
|
package/.claude/settings.json
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"hooks": [
|
|
53
53
|
{
|
|
54
54
|
"type": "prompt",
|
|
55
|
-
"prompt": "
|
|
55
|
+
"prompt": "Session context: $ARGUMENTS\n\nIf stop_hook_active is true in the context above, reply ONLY: {\"decision\":\"allow\"} to prevent infinite loops.\n\nOtherwise check: any uncommitted important changes? any in-progress task with outdated progress file? any unrecorded blockers?\n\nReply with ONLY valid JSON, no other text:\n- {\"decision\":\"block\",\"reason\":\"<describe what needs attention>\"} if action needed\n- {\"decision\":\"allow\"} if all clear"
|
|
56
56
|
}
|
|
57
57
|
]
|
|
58
58
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"WebFetch(domain:docs.anthropic.com)",
|
|
7
7
|
"Bash(grep -rL \"^name:\" .claude/skills/dw-*/SKILL.md)",
|
|
8
8
|
"Bash(grep -c '\\\\$ARGUMENTS\\\\|context: fork\\\\|allowed-tools' .dw/adapters/generic/AGENT.md)",
|
|
9
|
-
"Bash(grep [v1.0.0] CHANGELOG.md)"
|
|
9
|
+
"Bash(grep [v1.0.0] CHANGELOG.md)",
|
|
10
|
+
"Bash(node src/smoke-test.mjs)"
|
|
10
11
|
]
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dw-prompt
|
|
3
|
+
description: "Improve a vague task description into a clear, actionable prompt. Uses git log + recent dw tasks for project context. Output is concise — human dev will refine further."
|
|
4
|
+
argument-hint: "[task description] [--vi]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Prompt Builder
|
|
8
|
+
|
|
9
|
+
Input: **$ARGUMENTS**
|
|
10
|
+
|
|
11
|
+
## Bước 0 — Parse options
|
|
12
|
+
|
|
13
|
+
- Nếu `$ARGUMENTS` chứa `--vi`: output bằng **tiếng Việt**, bỏ flag `--vi` ra khỏi description
|
|
14
|
+
- Mặc định: output bằng **tiếng Anh**
|
|
15
|
+
|
|
16
|
+
## Bước 1 — Lấy context từ git log
|
|
17
|
+
|
|
18
|
+
Extract 1–2 keywords chính từ description (bỏ qua stop words: fix, add, feat, the, a, in, of).
|
|
19
|
+
|
|
20
|
+
Chạy **cả hai** để có context tốt nhất:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Tìm commits liên quan theo keyword
|
|
24
|
+
git log --oneline --no-merges --all --grep="<keyword1>" -15
|
|
25
|
+
git log --oneline --no-merges --all --grep="<keyword2>" -15
|
|
26
|
+
|
|
27
|
+
# Fallback: 30 commits gần nhất
|
|
28
|
+
git log --oneline --no-merges -30
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Dùng kết quả để nhận ra: module names, naming conventions, commit style của project.
|
|
32
|
+
|
|
33
|
+
## Bước 2 — Lấy context từ dw tasks gần đây
|
|
34
|
+
|
|
35
|
+
Đọc danh sách task đang/đã làm:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ls .dw/tasks/
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Nếu có task liên quan đến keyword → đọc file `*-progress.md` để hiểu thêm context (scope, decisions, findings).
|
|
42
|
+
|
|
43
|
+
## Bước 3 — Improve prompt
|
|
44
|
+
|
|
45
|
+
**Nếu $ARGUMENTS rỗng (sau khi bỏ flags):** hỏi ngắn "Describe your task:" trước.
|
|
46
|
+
|
|
47
|
+
**Rules:**
|
|
48
|
+
- **1–2 dòng tối đa** — human dev sẽ tự sửa thêm
|
|
49
|
+
- Giữ: **what** + **scope** (nếu rõ từ context) + **outcome** (nếu rõ)
|
|
50
|
+
- Active voice, present tense: "Fix...", "Add...", "Refactor..."
|
|
51
|
+
- Không bullet points, không markdown headers trong output
|
|
52
|
+
- Match naming conventions từ git log nếu nhận ra được
|
|
53
|
+
|
|
54
|
+
## Output format
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
─── Improved prompt ──────────────────────
|
|
58
|
+
<1–2 line improved prompt>
|
|
59
|
+
──────────────────────────────────────────
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Không thêm gì khác ngoài block trên.
|
package/CLAUDE.md
CHANGED
|
@@ -42,6 +42,7 @@ Không chắc scope → dùng `standard`. Assess dựa trên facts (file count,
|
|
|
42
42
|
|
|
43
43
|
| Skill | Mô tả | Depth |
|
|
44
44
|
|-------|--------|-------|
|
|
45
|
+
| `/dw-prompt [desc]` | Build structured prompt (autocomplete + wizard) | all |
|
|
45
46
|
| `/dw-task-init [name]` | Khởi tạo task docs | all |
|
|
46
47
|
| `/dw-research [name]` | Khảo sát codebase | all |
|
|
47
48
|
| `/dw-plan [name]` | Lập kế hoạch (DỪNG chờ approve) | standard+ |
|
package/README.md
CHANGED
|
@@ -19,15 +19,15 @@ Initialize → Understand → Plan → Execute (TDD) → Verify → Close
|
|
|
19
19
|
flowchart LR
|
|
20
20
|
classDef extra fill:#f3f4f6,stroke:#9ca3af,stroke-width:1px,color:#111;
|
|
21
21
|
|
|
22
|
-
D[Init + Understand] --> P[Plan (approve)]
|
|
23
|
-
P -->|approved| E[Execute (TDD)]
|
|
22
|
+
D["Init + Understand"] --> P["Plan (approve)"]
|
|
23
|
+
P -->|approved| E["Execute (TDD)"]
|
|
24
24
|
P -->|revise| P
|
|
25
25
|
|
|
26
|
-
E --> V[Verify (gates)]
|
|
27
|
-
V -->|sign-off| C[Close (handoff + archive)]
|
|
28
|
-
V -->|revise (fix)| E
|
|
26
|
+
E --> V["Verify (gates)"]
|
|
27
|
+
V -->|sign-off| C["Close (handoff + archive)"]
|
|
28
|
+
V -->|"revise (fix)"| E
|
|
29
29
|
|
|
30
|
-
subgraph Extra[Depth=thorough]
|
|
30
|
+
subgraph Extra["Depth=thorough"]
|
|
31
31
|
R[Req] --> Est[Est] --> AR[Arch] --> P
|
|
32
32
|
P -.-> TP[Test] -.-> E
|
|
33
33
|
E -.-> DU[Docs] --> LW[Log] -.-> C
|
|
@@ -36,6 +36,12 @@ flowchart LR
|
|
|
36
36
|
class R,Est,AR,TP,DU,LW extra
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Workflow overview
|
|
40
|
+
|
|
41
|
+
`dw` runs a 6-phase process (all phases for `standard` and `thorough`):
|
|
42
|
+
|
|
43
|
+
Initialize → Understand → Plan (stops for approval) → Execute (TDD; 1 commit per subtask) → Verify (quality gates + review sign-off) → Close (handoff + archive when done).
|
|
44
|
+
|
|
39
45
|
### 6 phases (full workflow)
|
|
40
46
|
- **Initialize**: clarify task scope and set up the workspace + task docs.
|
|
41
47
|
- **Understand**: survey the codebase, dependencies, patterns, and test coverage (no implementation).
|
|
@@ -58,24 +64,28 @@ npm install -g dw-kit
|
|
|
58
64
|
|
|
59
65
|
## Quick start
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
Setup dw in project directory:
|
|
62
68
|
|
|
63
69
|
```bash
|
|
64
70
|
dw init
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
Then in **Claude Code
|
|
73
|
+
Then in **Claude Code CLI**, run the full workflow:
|
|
68
74
|
|
|
69
75
|
```
|
|
70
|
-
/dw-flow
|
|
76
|
+
/dw-flow your-task-or-anythings
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
|
|
79
|
+
---
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
Discover other skills:
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
```
|
|
84
|
+
/dw-prompt
|
|
85
|
+
/dw-thinking
|
|
86
|
+
...
|
|
78
87
|
|
|
88
|
+
```
|
|
79
89
|
|
|
80
90
|
---
|
|
81
91
|
|
|
@@ -88,8 +98,13 @@ dw doctor # installation health check
|
|
|
88
98
|
dw upgrade # update toolkit files (override-aware)
|
|
89
99
|
dw upgrade --check # check for updates only
|
|
90
100
|
dw upgrade --dry-run # preview changes
|
|
101
|
+
dw prompt # build a well-structured task prompt (autocomplete + wizard)
|
|
102
|
+
dw prompt --text "..." # non-interactive: structure a description directly
|
|
103
|
+
dw claude-vn-fix # patch Claude CLI to fix Vietnamese IME (backup/restore)
|
|
91
104
|
```
|
|
92
105
|
|
|
106
|
+
`dw claude-vn-fix` patches the local Claude CLI bundle to fix Vietnamese IME input (DEL char `\x7f` issue). Includes auto-backup and rollback.
|
|
107
|
+
|
|
93
108
|
---
|
|
94
109
|
|
|
95
110
|
## Depth system
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI development workflow toolkit — structured, quality-assured, team-ready. From requirements to dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"ajv": "^8.18.0",
|
|
47
47
|
"chalk": "^5.6.2",
|
|
48
48
|
"commander": "^14.0.3",
|
|
49
|
+
"enquirer": "^2.4.1",
|
|
49
50
|
"js-yaml": "^4.1.1"
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Minimal fixture that contains the known Vietnamese IME bug pattern.
|
|
2
|
+
// This is NOT the real Claude CLI; only used for testing the patcher logic.
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: The comment below must contain both '@anthropic-ai' and 'claude-code'
|
|
5
|
+
// to pass the bundle signature guard in patchCliJs(). Do not remove it.
|
|
6
|
+
// @anthropic-ai/claude-code bundle stub
|
|
7
|
+
|
|
8
|
+
function demo(INPUT) {
|
|
9
|
+
if(INPUT.includes("\x7f")){
|
|
10
|
+
let COUNT=(INPUT.match(/\x7f/g)||[]).length,STATE=CURSTATE;
|
|
11
|
+
UPDATETEXT(STATE.text);UPDATEOFFSET(STATE.offset)
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
package/src/cli.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { getUpdateNotice, scheduleUpdateCheck } from './lib/update-checker.mjs';
|
|
5
7
|
|
|
6
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
9
|
const __dirname = dirname(__filename);
|
|
@@ -9,6 +11,10 @@ const require = createRequire(import.meta.url);
|
|
|
9
11
|
const pkg = require(join(__dirname, '..', 'package.json'));
|
|
10
12
|
|
|
11
13
|
export function run(argv) {
|
|
14
|
+
// Show cached update notice (non-blocking), then schedule fresh check in background
|
|
15
|
+
const latestVersion = getUpdateNotice(pkg.version);
|
|
16
|
+
scheduleUpdateCheck(pkg.version);
|
|
17
|
+
|
|
12
18
|
const program = new Command();
|
|
13
19
|
|
|
14
20
|
program
|
|
@@ -55,5 +61,32 @@ export function run(argv) {
|
|
|
55
61
|
await doctorCommand();
|
|
56
62
|
});
|
|
57
63
|
|
|
64
|
+
program
|
|
65
|
+
.command('prompt')
|
|
66
|
+
.description('Build a well-structured task prompt with autocomplete + guided wizard')
|
|
67
|
+
.option('-t, --text <text>', 'Non-interactive: provide description directly')
|
|
68
|
+
.action(async (opts) => {
|
|
69
|
+
const { promptCommand } = await import('./commands/prompt.mjs');
|
|
70
|
+
await promptCommand(opts);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('claude-vn-fix')
|
|
75
|
+
.description('Patch Claude CLI to fix Vietnamese IME (local, with backup/restore)')
|
|
76
|
+
.option('--path <file>', 'Path to @anthropic-ai/claude-code/cli.js (optional; auto-detect if omitted)')
|
|
77
|
+
.option('--restore', 'Restore from latest backup')
|
|
78
|
+
.option('--dry-run', 'Show what would change without writing')
|
|
79
|
+
.action(async (opts) => {
|
|
80
|
+
const { claudeVnFixCommand } = await import('./commands/claude-vn-fix.mjs');
|
|
81
|
+
await claudeVnFixCommand(opts);
|
|
82
|
+
});
|
|
83
|
+
|
|
58
84
|
program.parse(argv);
|
|
85
|
+
|
|
86
|
+
if (latestVersion) {
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(chalk.yellow(` ↑ Update available`) + ` v${pkg.version} → ` + chalk.green.bold(`v${latestVersion}`));
|
|
89
|
+
console.log(` Run ` + chalk.cyan(`npm install -g dw-kit`) + ` to update`);
|
|
90
|
+
console.log();
|
|
91
|
+
}
|
|
59
92
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { header, info, ok, warn, err, log } from '../lib/ui.mjs';
|
|
6
|
+
|
|
7
|
+
export const PATCH_MARKER = '/* dw-kit Vietnamese IME fix */';
|
|
8
|
+
export const DEL_CHAR = '\x7f';
|
|
9
|
+
|
|
10
|
+
export async function claudeVnFixCommand(opts) {
|
|
11
|
+
header('dw-kit Claude Vietnamese IME Fix');
|
|
12
|
+
|
|
13
|
+
const filePath = opts.path ? opts.path : findCliJs();
|
|
14
|
+
log(`Target file: ${filePath}`);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(filePath)) {
|
|
17
|
+
err(`File not found: ${filePath}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (opts.restore) {
|
|
22
|
+
info('Restoring from latest backup');
|
|
23
|
+
const restored = restoreLatestBackup(filePath, { dryRun: !!opts.dryRun });
|
|
24
|
+
if (!restored) process.exit(1);
|
|
25
|
+
ok('Restore complete. Restart Claude CLI.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
info('Patching');
|
|
30
|
+
const result = patchCliJs(filePath, { dryRun: !!opts.dryRun });
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
err(result.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
ok(result.message);
|
|
36
|
+
log('Restart Claude CLI for changes to take effect.');
|
|
37
|
+
warn('Note: This modifies a third-party installed file; you may need to re-run after Claude updates.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findCliJs() {
|
|
41
|
+
// Strategy: check global npm root first, then common cache locations.
|
|
42
|
+
// This is intentionally conservative (no deep recursive scan of entire home).
|
|
43
|
+
const candidates = [];
|
|
44
|
+
|
|
45
|
+
// npm root -g
|
|
46
|
+
const npmRoot = tryNpmRootGlobal();
|
|
47
|
+
if (npmRoot) {
|
|
48
|
+
candidates.push(join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const home = os.homedir();
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const appData = process.env.APPDATA || '';
|
|
54
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
55
|
+
if (appData) candidates.push(join(appData, 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'));
|
|
56
|
+
if (localAppData) {
|
|
57
|
+
const npxBase = join(localAppData, 'npm-cache', '_npx');
|
|
58
|
+
const latest = findLatestNpxClaudeCli(npxBase);
|
|
59
|
+
if (latest) candidates.push(latest);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const npxLatest = findLatestNpxClaudeCli(join(home, '.npm', '_npx'));
|
|
63
|
+
if (npxLatest) candidates.push(npxLatest);
|
|
64
|
+
candidates.push('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js');
|
|
65
|
+
candidates.push('/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const p of candidates) {
|
|
69
|
+
if (p && existsSync(p) && statSync(p).isFile()) return p;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Could not auto-detect @anthropic-ai/claude-code/cli.js.\n' +
|
|
74
|
+
'Provide it explicitly via: dw claude-vn-fix --path "<path-to-cli.js>"',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tryNpmRootGlobal() {
|
|
79
|
+
try {
|
|
80
|
+
const out = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8', timeout: 5000 });
|
|
81
|
+
return out.trim();
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function safeMtime(p) {
|
|
88
|
+
try { return statSync(p).mtimeMs || 0; } catch { return 0; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findLatestNpxClaudeCli(npxBase) {
|
|
92
|
+
try {
|
|
93
|
+
if (!npxBase || !existsSync(npxBase)) return null;
|
|
94
|
+
const entries = readdirSync(npxBase, { withFileTypes: true })
|
|
95
|
+
.filter((e) => e.isDirectory())
|
|
96
|
+
.map((e) => join(npxBase, e.name));
|
|
97
|
+
const sorted = entries
|
|
98
|
+
.map((d) => ({ d, t: safeMtime(d) }))
|
|
99
|
+
.sort((a, b) => b.t - a.t)
|
|
100
|
+
.map((x) => x.d);
|
|
101
|
+
|
|
102
|
+
for (const dir of sorted.slice(0, 50)) {
|
|
103
|
+
const p = join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
|
|
104
|
+
if (existsSync(p)) return p;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function patchCliJs(filePath, { dryRun }) {
|
|
113
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
114
|
+
|
|
115
|
+
// Guard: ensure this is actually a Claude CLI bundle before patching.
|
|
116
|
+
if (!content.includes('@anthropic-ai') || !content.includes('claude-code')) {
|
|
117
|
+
return { ok: false, message: 'File does not appear to be a Claude CLI bundle. Use --path to specify the correct file.' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (content.includes(PATCH_MARKER)) {
|
|
121
|
+
return { ok: true, message: 'Already patched (marker found).' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const idx = findBugPatternIndex(content);
|
|
125
|
+
if (idx === -1) {
|
|
126
|
+
return { ok: false, message: 'Bug pattern not found (.includes("\\x7f")). Claude may already be fixed upstream.' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { start, end, block } = findIfBlock(content, idx);
|
|
130
|
+
const vars = extractVariables(block);
|
|
131
|
+
const fixCode = generateFix(vars);
|
|
132
|
+
const patched = content.slice(0, start) + fixCode + content.slice(end);
|
|
133
|
+
|
|
134
|
+
if (dryRun) {
|
|
135
|
+
log('DRY RUN: would create backup and patch file.');
|
|
136
|
+
log(`Detected vars: input=${vars.input}, state=${vars.state}, cur=${vars.curState}`);
|
|
137
|
+
return { ok: true, message: 'Dry run OK.' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const backupPath = createBackup(filePath);
|
|
141
|
+
ok(`Backup created: ${backupPath}`);
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(filePath, patched, 'utf-8');
|
|
144
|
+
const verify = readFileSync(filePath, 'utf-8');
|
|
145
|
+
if (!verify.includes(PATCH_MARKER)) throw new Error('verify failed (marker missing after write)');
|
|
146
|
+
return { ok: true, message: `Patch applied. Backup: ${backupPath}` };
|
|
147
|
+
} catch (e) {
|
|
148
|
+
warn(`Patch failed: ${e.message}`);
|
|
149
|
+
warn('Rolling back from backup.');
|
|
150
|
+
try {
|
|
151
|
+
copyFileSync(backupPath, filePath);
|
|
152
|
+
} catch (rollbackErr) {
|
|
153
|
+
return { ok: false, message: `Patch failed AND rollback failed: ${rollbackErr.message}. Manual restore from: ${backupPath}` };
|
|
154
|
+
}
|
|
155
|
+
return { ok: false, message: `Patch failed and rolled back: ${e.message}` };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findBugPatternIndex(content) {
|
|
160
|
+
// Claude builds may contain either:
|
|
161
|
+
// - literal escape sequence: ".includes(\"\\x7f\")"
|
|
162
|
+
// - actual DEL char 0x7f inside the string: ".includes(\"\")"
|
|
163
|
+
const literal = content.indexOf('.includes("\\x7f")');
|
|
164
|
+
if (literal !== -1) return literal;
|
|
165
|
+
return content.indexOf(`.includes("${DEL_CHAR}")`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createBackup(filePath) {
|
|
169
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
170
|
+
const backupPath = `${filePath}.backup-${ts}`;
|
|
171
|
+
copyFileSync(filePath, backupPath);
|
|
172
|
+
return backupPath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function restoreLatestBackup(filePath, { dryRun }) {
|
|
176
|
+
const dir = dirname(filePath);
|
|
177
|
+
const base = filePath.split(/[\\/]/).pop();
|
|
178
|
+
const backups = readdirSync(dir)
|
|
179
|
+
.filter((f) => f.startsWith(`${base}.backup-`))
|
|
180
|
+
.map((f) => join(dir, f))
|
|
181
|
+
.map((p) => ({ p, t: safeMtime(p) }))
|
|
182
|
+
.sort((a, b) => b.t - a.t);
|
|
183
|
+
|
|
184
|
+
const latest = backups[0]?.p;
|
|
185
|
+
if (!latest) {
|
|
186
|
+
err('No backups found to restore.');
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
log(`DRY RUN: would restore from ${latest}`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
copyFileSync(latest, filePath);
|
|
196
|
+
ok(`Restored from: ${latest}`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findIfBlock(content, idx) {
|
|
201
|
+
// Search backward from idx within a 500-char window to find the nearest if(
|
|
202
|
+
const windowStart = Math.max(0, idx - 500);
|
|
203
|
+
const searchSlice = content.slice(windowStart, idx);
|
|
204
|
+
const localOffset = searchSlice.lastIndexOf('if(');
|
|
205
|
+
if (localOffset === -1) throw new Error(`Could not find containing if(...) block near index ${idx}`);
|
|
206
|
+
const start = windowStart + localOffset;
|
|
207
|
+
|
|
208
|
+
let depth = 0;
|
|
209
|
+
let end = -1;
|
|
210
|
+
for (let i = start; i < content.length; i++) {
|
|
211
|
+
const c = content[i];
|
|
212
|
+
if (c === '{') depth++;
|
|
213
|
+
else if (c === '}') {
|
|
214
|
+
depth--;
|
|
215
|
+
if (depth === 0) { end = i + 1; break; }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (end === -1) throw new Error('Could not find end of if block (brace mismatch)');
|
|
219
|
+
if (idx < start || idx > end) throw new Error('Bug pattern found outside expected if block');
|
|
220
|
+
return { start, end, block: content.slice(start, end) };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractVariables(block) {
|
|
224
|
+
// Normalize DEL char for regex matching
|
|
225
|
+
const normalized = block.replaceAll(DEL_CHAR, '\\x7f');
|
|
226
|
+
|
|
227
|
+
// Match: let COUNT=(INPUT.match(/\x7f/g)||[]).length,STATE=CURSTATE;
|
|
228
|
+
const m = normalized.match(/let ([\w$]+)=\(\w+\.match\(\/\\x7f\/g\)\|\|\[\]\)\.length[,;]([\w$]+)=([\w$]+)[;,]/);
|
|
229
|
+
if (!m) throw new Error('Could not extract variables (count/state/curState)');
|
|
230
|
+
const state = m[2];
|
|
231
|
+
const curState = m[3];
|
|
232
|
+
|
|
233
|
+
const m2 = block.match(new RegExp(`([\\w$]+)\\(${escapeRegex(state)}\\.text\\);([\\w$]+)\\(${escapeRegex(state)}\\.offset\\)`));
|
|
234
|
+
if (!m2) throw new Error('Could not extract update functions');
|
|
235
|
+
|
|
236
|
+
const m3 = block.match(/([\w$]+)\.includes\("/);
|
|
237
|
+
if (!m3) throw new Error('Could not extract input variable');
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
input: m3[1],
|
|
241
|
+
state,
|
|
242
|
+
curState,
|
|
243
|
+
updateText: m2[1],
|
|
244
|
+
updateOffset: m2[2],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function generateFix(v) {
|
|
249
|
+
// This mirrors the known fix: backspace N times, then insert replacement text.
|
|
250
|
+
return (
|
|
251
|
+
`${PATCH_MARKER}` +
|
|
252
|
+
`if(${v.input}.includes("\\x7f")){` +
|
|
253
|
+
`let _n=(${v.input}.match(/\\x7f/g)||[]).length,` +
|
|
254
|
+
`_vn=${v.input}.replace(/\\x7f/g,""),` +
|
|
255
|
+
`${v.state}=${v.curState};` +
|
|
256
|
+
`for(let _i=0;_i<_n;_i++)${v.state}=${v.state}.backspace();` +
|
|
257
|
+
`for(const _c of _vn)${v.state}=${v.state}.insert(_c);` +
|
|
258
|
+
`if(!${v.curState}.equals(${v.state})){` +
|
|
259
|
+
`if(${v.curState}.text!==${v.state}.text)${v.updateText}(${v.state}.text);` +
|
|
260
|
+
`${v.updateOffset}(${v.state}.offset)` +
|
|
261
|
+
`}return;}`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function escapeRegex(s) {
|
|
266
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
267
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
4
|
+
import { header, info, ok, warn, err, log } from '../lib/ui.mjs';
|
|
5
|
+
import { copyToClipboard } from '../lib/clipboard.mjs';
|
|
6
|
+
import { getSuggestions, isVague, expandTemplate } from '../lib/prompt-suggest.mjs';
|
|
7
|
+
|
|
8
|
+
export async function promptCommand(opts) {
|
|
9
|
+
header('dw-kit Prompt Builder');
|
|
10
|
+
|
|
11
|
+
const adapter = readAdapter();
|
|
12
|
+
|
|
13
|
+
// Non-interactive mode: --text <text>
|
|
14
|
+
if (opts.text !== undefined) {
|
|
15
|
+
if (!opts.text.trim()) {
|
|
16
|
+
err('--text cannot be empty.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const result = await buildPrompt(opts.text, { interactive: false });
|
|
20
|
+
outputResult(result, adapter);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Interactive mode
|
|
25
|
+
const result = await buildPrompt('', { interactive: true });
|
|
26
|
+
outputResult(result, adapter);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildPrompt(initialText, { interactive }) {
|
|
30
|
+
let description = initialText;
|
|
31
|
+
|
|
32
|
+
if (interactive) {
|
|
33
|
+
description = await runAutocompleteStep();
|
|
34
|
+
if (!description.trim()) {
|
|
35
|
+
err('No description provided.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let area = '';
|
|
41
|
+
let outcome = '';
|
|
42
|
+
|
|
43
|
+
if (isVague(description)) {
|
|
44
|
+
if (interactive) {
|
|
45
|
+
info('Description seems short — a couple of quick questions (Enter to skip):');
|
|
46
|
+
({ area, outcome } = await runWizardStep());
|
|
47
|
+
}
|
|
48
|
+
// In non-interactive mode: expand with just the description (no wizard data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return expandTemplate(description, { area, outcome });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runAutocompleteStep() {
|
|
55
|
+
const { AutoComplete } = await import('enquirer');
|
|
56
|
+
const suggestions = getSuggestions(process.cwd());
|
|
57
|
+
|
|
58
|
+
const prompt = new AutoComplete({
|
|
59
|
+
name: 'description',
|
|
60
|
+
message: 'Describe your task:',
|
|
61
|
+
limit: 7,
|
|
62
|
+
choices: suggestions.length ? suggestions : ['(no suggestions — type your task)'],
|
|
63
|
+
suggest(typed, choices) {
|
|
64
|
+
const lower = typed.toLowerCase();
|
|
65
|
+
const filtered = choices.filter((c) => c.message.toLowerCase().includes(lower));
|
|
66
|
+
// Always offer the raw typed text as first selectable option
|
|
67
|
+
if (typed && !filtered.find((c) => c.message === typed)) {
|
|
68
|
+
return [{ message: typed, value: typed }, ...filtered];
|
|
69
|
+
}
|
|
70
|
+
return filtered.length ? filtered : [{ message: typed || '(empty)', value: typed }];
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return prompt.run();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runWizardStep() {
|
|
78
|
+
const { Input } = await import('enquirer');
|
|
79
|
+
|
|
80
|
+
const area = await new Input({
|
|
81
|
+
name: 'area',
|
|
82
|
+
message: 'Which area/files? (e.g. auth middleware, src/login/)',
|
|
83
|
+
initial: '',
|
|
84
|
+
}).run().catch(() => '');
|
|
85
|
+
|
|
86
|
+
const outcome = await new Input({
|
|
87
|
+
name: 'outcome',
|
|
88
|
+
message: 'Expected outcome? (e.g. user redirected to /dashboard)',
|
|
89
|
+
initial: '',
|
|
90
|
+
}).run().catch(() => '');
|
|
91
|
+
|
|
92
|
+
return { area, outcome };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outputResult(text, adapter) {
|
|
96
|
+
log('');
|
|
97
|
+
log('─── Result ───────────────────────────────────────────');
|
|
98
|
+
log(text);
|
|
99
|
+
log('──────────────────────────────────────────────────────');
|
|
100
|
+
log('');
|
|
101
|
+
|
|
102
|
+
const shouldCopy = adapter === 'claude-cli' || adapter === 'cursor';
|
|
103
|
+
if (shouldCopy) {
|
|
104
|
+
const copied = copyToClipboard(text);
|
|
105
|
+
if (copied) {
|
|
106
|
+
ok('Copied to clipboard. Paste into Claude CLI or your IDE.');
|
|
107
|
+
} else {
|
|
108
|
+
warn('Clipboard copy failed. Copy the text above manually.');
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// generic adapter: just output to stdout
|
|
112
|
+
info('(generic adapter — copy the text above manually)');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readAdapter() {
|
|
117
|
+
const configPath = join(process.cwd(), '.dw', 'config', 'dw.config.yml');
|
|
118
|
+
if (!existsSync(configPath)) return 'claude-cli';
|
|
119
|
+
try {
|
|
120
|
+
const config = yamlLoad(readFileSync(configPath, 'utf-8'));
|
|
121
|
+
return config?.adapter || 'claude-cli';
|
|
122
|
+
} catch {
|
|
123
|
+
return 'claude-cli';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { platform } from 'node:process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copy text to system clipboard.
|
|
6
|
+
* @returns {boolean} true if succeeded
|
|
7
|
+
*/
|
|
8
|
+
export function copyToClipboard(text) {
|
|
9
|
+
const candidates = platform === 'win32'
|
|
10
|
+
? [['clip']]
|
|
11
|
+
: platform === 'darwin'
|
|
12
|
+
? [['pbcopy']]
|
|
13
|
+
: [['wl-copy'], ['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input']];
|
|
14
|
+
|
|
15
|
+
for (const [cmd, ...args] of candidates) {
|
|
16
|
+
try {
|
|
17
|
+
const result = spawnSync(cmd, args, { input: text, encoding: 'utf-8' });
|
|
18
|
+
if (result.status === 0) return true;
|
|
19
|
+
} catch {
|
|
20
|
+
// Try next candidate.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const TEMPLATE_SUGGESTIONS = [
|
|
4
|
+
'fix: ',
|
|
5
|
+
'fix authentication redirect after login',
|
|
6
|
+
'fix null pointer / undefined error in ',
|
|
7
|
+
'fix performance issue in ',
|
|
8
|
+
'feat: add ',
|
|
9
|
+
'feat: implement ',
|
|
10
|
+
'feat: support ',
|
|
11
|
+
'refactor: simplify ',
|
|
12
|
+
'refactor: extract ',
|
|
13
|
+
'refactor: rename ',
|
|
14
|
+
'perf: optimize ',
|
|
15
|
+
'perf: reduce load time of ',
|
|
16
|
+
'chore: update dependencies',
|
|
17
|
+
'docs: update README',
|
|
18
|
+
'test: add tests for ',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Common verbs to detect whether a description has intent
|
|
22
|
+
const INTENT_VERBS = [
|
|
23
|
+
'fix', 'add', 'update', 'remove', 'delete', 'create', 'implement', 'refactor',
|
|
24
|
+
'rename', 'move', 'improve', 'optimize', 'support', 'extract', 'migrate',
|
|
25
|
+
'replace', 'upgrade', 'enable', 'disable', 'integrate', 'patch',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function getTemplateSuggestions() {
|
|
29
|
+
return [...TEMPLATE_SUGGESTIONS];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getGitSuggestions(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const out = execSync('git log --oneline -50 --no-merges', {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: 3000,
|
|
38
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
39
|
+
});
|
|
40
|
+
return out
|
|
41
|
+
.trim()
|
|
42
|
+
.split('\n')
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map((line) => line.replace(/^[a-f0-9]+ /, '').trim()) // strip hash
|
|
45
|
+
.filter((msg) => msg.length > 5)
|
|
46
|
+
.slice(0, 30);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSuggestions(cwd = process.cwd()) {
|
|
53
|
+
const git = getGitSuggestions(cwd);
|
|
54
|
+
const templates = getTemplateSuggestions();
|
|
55
|
+
// git log first (most relevant to this repo), then templates
|
|
56
|
+
const merged = [...git, ...templates];
|
|
57
|
+
// dedupe
|
|
58
|
+
return [...new Set(merged)].slice(0, 20);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the description is likely too vague to give Claude good context.
|
|
63
|
+
*/
|
|
64
|
+
export function isVague(text) {
|
|
65
|
+
const trimmed = (text || '').trim();
|
|
66
|
+
if (trimmed.length < 50) return true;
|
|
67
|
+
const lower = trimmed.toLowerCase();
|
|
68
|
+
const hasVerb = INTENT_VERBS.some((v) => lower.startsWith(v) || lower.includes(` ${v} `));
|
|
69
|
+
return !hasVerb;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Expand a short description into a structured prompt.
|
|
74
|
+
* @param {string} text - core task description
|
|
75
|
+
* @param {{ area?: string, outcome?: string }} extras
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function expandTemplate(text, { area = '', outcome = '' } = {}) {
|
|
79
|
+
const base = text.trim();
|
|
80
|
+
const parts = [base];
|
|
81
|
+
if (area) parts.push(`Scope: ${area.trim()}.`);
|
|
82
|
+
if (outcome) parts.push(`Expected: ${outcome.trim()}.`);
|
|
83
|
+
return parts.join('\n');
|
|
84
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/dw-kit/latest';
|
|
7
|
+
const CACHE_DIR = join(homedir(), '.dw-kit');
|
|
8
|
+
const CACHE_FILE = join(CACHE_DIR, 'update-cache.json');
|
|
9
|
+
|
|
10
|
+
function parseSemver(v) {
|
|
11
|
+
const parts = String(v).replace(/^v/, '').split('.').map(Number);
|
|
12
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isNewer(latest, current) {
|
|
16
|
+
const l = parseSemver(latest);
|
|
17
|
+
const c = parseSemver(current);
|
|
18
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
19
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
20
|
+
return l.patch > c.patch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readCache() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeCache(data) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
34
|
+
writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore write errors (permission, disk full, etc.)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchLatestVersion() {
|
|
41
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(3000) });
|
|
42
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data.version;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns latest version string if an update is available (from cache), null otherwise.
|
|
49
|
+
* Never throws, never makes network calls.
|
|
50
|
+
*/
|
|
51
|
+
export function getUpdateNotice(currentVersion) {
|
|
52
|
+
if (process.env.DW_NO_UPDATE_CHECK) return null;
|
|
53
|
+
const cache = readCache();
|
|
54
|
+
if (!cache?.latest) return null;
|
|
55
|
+
return isNewer(cache.latest, currentVersion) ? cache.latest : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fires off an async check against npm registry and updates the cache.
|
|
60
|
+
* Non-blocking — caller should NOT await this.
|
|
61
|
+
* Skips the check if cache is still fresh (< 24h).
|
|
62
|
+
*/
|
|
63
|
+
export function scheduleUpdateCheck(currentVersion) {
|
|
64
|
+
if (process.env.DW_NO_UPDATE_CHECK) return;
|
|
65
|
+
|
|
66
|
+
const cache = readCache();
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (cache?.checkedAt && now - cache.checkedAt < CACHE_TTL_MS) return;
|
|
69
|
+
|
|
70
|
+
fetchLatestVersion()
|
|
71
|
+
.then((latest) => writeCache({ latest, checkedAt: now, current: currentVersion }))
|
|
72
|
+
.catch(() => {});
|
|
73
|
+
}
|
package/src/smoke-test.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { copyFileSync } from 'node:fs';
|
|
14
15
|
|
|
15
16
|
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
|
|
16
17
|
const DW_BIN = resolve(__dirname, '..', 'bin', 'dw.mjs');
|
|
@@ -72,7 +73,7 @@ test('--version returns semver', () => {
|
|
|
72
73
|
|
|
73
74
|
test('--help lists all commands', () => {
|
|
74
75
|
const out = dw('--help', TEMP_BASE);
|
|
75
|
-
for (const cmd of ['init', 'upgrade', 'validate', 'doctor']) {
|
|
76
|
+
for (const cmd of ['init', 'upgrade', 'validate', 'doctor', 'prompt', 'claude-vn-fix']) {
|
|
76
77
|
assert(out.includes(cmd), `Missing command: ${cmd}`);
|
|
77
78
|
}
|
|
78
79
|
});
|
|
@@ -262,6 +263,51 @@ test('doctor reports issues on empty project', () => {
|
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
265
|
|
|
266
|
+
// ── Test: dw prompt ──────────────────────────────────────────────────────────
|
|
267
|
+
console.log();
|
|
268
|
+
console.log('▶ dw prompt');
|
|
269
|
+
|
|
270
|
+
test('prompt --text outputs structured result', () => {
|
|
271
|
+
const out = dw('prompt --text "fix login redirect after OAuth in auth middleware"', TEMP_BASE);
|
|
272
|
+
assert(out.includes('fix login redirect'), 'Should include description in output');
|
|
273
|
+
assert(!out.includes('Description seems short'), 'Long description should skip wizard');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('prompt --text with short input expands without error', () => {
|
|
277
|
+
const out = dw('prompt --text "fix login"', TEMP_BASE);
|
|
278
|
+
assert(out.includes('fix login'), 'Should include description in output');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('prompt --text empty string exits with error', () => {
|
|
282
|
+
try {
|
|
283
|
+
dw('prompt --text ""', TEMP_BASE);
|
|
284
|
+
assert(false, 'Should have thrown');
|
|
285
|
+
} catch (e) {
|
|
286
|
+
assert(e.status === 1, 'Should exit with code 1');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('prompt --help shows options', () => {
|
|
291
|
+
const out = dw('prompt --help', TEMP_BASE);
|
|
292
|
+
assert(out.includes('--text'), 'Missing --text option');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── Test: dw claude-vn-fix (fixture patch) ───────────────────────────────────
|
|
296
|
+
console.log();
|
|
297
|
+
console.log('▶ dw claude-vn-fix');
|
|
298
|
+
|
|
299
|
+
test('claude-vn-fix patches known bug fixture', () => {
|
|
300
|
+
const dir = freshDir('claude-vn-fix-fixture');
|
|
301
|
+
const fixtureSrc = resolve(__dirname, '__fixtures__', 'claude-cli-bug-snippet.js');
|
|
302
|
+
const target = join(dir, 'cli.js');
|
|
303
|
+
copyFileSync(fixtureSrc, target);
|
|
304
|
+
|
|
305
|
+
const out = dw(`claude-vn-fix --path "${target}"`, dir);
|
|
306
|
+
assert(out.includes('Patch applied') || out.includes('Already patched'), 'Should apply patch');
|
|
307
|
+
const patched = readFileSync(target, 'utf-8');
|
|
308
|
+
assert(patched.includes('dw-kit Vietnamese IME fix'), 'Patch marker missing in output file');
|
|
309
|
+
});
|
|
310
|
+
|
|
265
311
|
// ── Test: dw upgrade ─────────────────────────────────────────────────────────
|
|
266
312
|
console.log();
|
|
267
313
|
console.log('▶ dw upgrade');
|