dw-kit 1.0.0 → 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/hooks/post-write.sh +1 -3
- 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/skills/dw-upgrade/SKILL.md +20 -30
- package/.dw/adapters/claude-cli/extensions/README.md +2 -2
- package/.dw/adapters/claude-cli/generated/README.md +3 -3
- package/.dw/adapters/claude-cli/overrides/README.md +4 -2
- package/CLAUDE.md +1 -0
- package/README.md +78 -122
- package/package.json +5 -5
- package/scripts/e2e-local-check.sh +1 -2
- package/src/__fixtures__/claude-cli-bug-snippet.js +15 -0
- package/src/cli.mjs +29 -5
- 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 -11
- package/scripts/migrate-v03-to-v1.sh +0 -243
- package/scripts/upgrade.sh +0 -246
- package/setup.sh +0 -382
- package/src/commands/migrate.mjs +0 -215
package/README.md
CHANGED
|
@@ -1,183 +1,139 @@
|
|
|
1
|
-
|
|
1
|
+
# dw-kit
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> An AI development workflow toolkit for teams using agentic IDEs (Claude Code, Cursor) — from idea to review-ready commits.
|
|
4
4
|
|
|
5
|
-
**v1.0** · `npm install -g dw-kit` · [Docs](docs/README.md) · [
|
|
5
|
+
**v1.0** · `npm install -g dw-kit` · [Docs](docs/README.md) · [Get started](docs/get-started.md) · [Cheatsheet](docs/cheatsheet.md)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What is dw-kit?
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
dw-kit helps your team run AI-assisted development with a **repeatable workflow** and clear checkpoints:
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
Initialize → Understand → Plan → Execute (TDD) → Verify → Close
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```mermaid
|
|
18
|
+
%%{init: {'flowchart': {'nodeSpacing': 10, 'rankSpacing': 18}}}%%
|
|
19
|
+
flowchart LR
|
|
20
|
+
classDef extra fill:#f3f4f6,stroke:#9ca3af,stroke-width:1px,color:#111;
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
D["Init + Understand"] --> P["Plan (approve)"]
|
|
23
|
+
P -->|approved| E["Execute (TDD)"]
|
|
24
|
+
P -->|revise| P
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
E --> V["Verify (gates)"]
|
|
27
|
+
V -->|sign-off| C["Close (handoff + archive)"]
|
|
28
|
+
V -->|"revise (fix)"| E
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
subgraph Extra["Depth=thorough"]
|
|
31
|
+
R[Req] --> Est[Est] --> AR[Arch] --> P
|
|
32
|
+
P -.-> TP[Test] -.-> E
|
|
33
|
+
E -.-> DU[Docs] --> LW[Log] -.-> C
|
|
34
|
+
end
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
npm install -g dw-kit
|
|
36
|
+
class R,Est,AR,TP,DU,LW extra
|
|
27
37
|
```
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
## Workflow overview
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
dw init
|
|
33
|
-
```
|
|
41
|
+
`dw` runs a 6-phase process (all phases for `standard` and `thorough`):
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
Initialize → Understand → Plan (stops for approval) → Execute (TDD; 1 commit per subtask) → Verify (quality gates + review sign-off) → Close (handoff + archive when done).
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx dw-kit init
|
|
47
|
-
```
|
|
45
|
+
### 6 phases (full workflow)
|
|
46
|
+
- **Initialize**: clarify task scope and set up the workspace + task docs.
|
|
47
|
+
- **Understand**: survey the codebase, dependencies, patterns, and test coverage (no implementation).
|
|
48
|
+
- **Plan**: design the solution and subtasks; **pause for your approval**.
|
|
49
|
+
- **Execute**: implement using **TDD**; each subtask produces a commit.
|
|
50
|
+
- **Verify**: run quality gates + review sign-off to ensure correctness and safety.
|
|
51
|
+
- **Close**: handoff notes, finalize progress, and archive when done.
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
It’s designed for collaboration (Dev / Tech Lead / QA / PM) and keeps work auditable via lightweight task docs.
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
git submodule add https://github.com/dv-workflow/dv-workflow.git .dw-module
|
|
53
|
-
bash .dw-module/setup.sh
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
`setup.sh` là luồng legacy/fallback. Luồng khuyến nghị cho v1 là `npm install -g dw-kit` + `dw init`.
|
|
57
|
-
|
|
58
|
-
### Start working
|
|
59
|
-
|
|
60
|
-
Open Claude Code in your project directory:
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
/dw-task-init tên-feature
|
|
64
|
-
```
|
|
55
|
+
---
|
|
65
56
|
|
|
66
|
-
|
|
57
|
+
## Install
|
|
67
58
|
|
|
68
59
|
```bash
|
|
69
|
-
|
|
70
|
-
dw upgrade # Update toolkit files (override-aware)
|
|
71
|
-
dw upgrade --dry-run # Preview changes
|
|
72
|
-
dw upgrade --check # Check for updates only
|
|
73
|
-
dw validate # Validate config against schema
|
|
74
|
-
dw doctor # Check installation health
|
|
75
|
-
dw migrate # Migrate from v0.3 to v1
|
|
60
|
+
npm install -g dw-kit
|
|
76
61
|
```
|
|
77
62
|
|
|
78
63
|
---
|
|
79
64
|
|
|
80
|
-
##
|
|
65
|
+
## Quick start
|
|
81
66
|
|
|
82
|
-
|
|
83
|
-
|-------|----------|----------|
|
|
84
|
-
| `quick` | Solo dev, hotfix, familiar code | Understand → Execute → Close |
|
|
85
|
-
| `standard` | Team nhỏ, feature mới | Tất cả 6 phases |
|
|
86
|
-
| `thorough` | Enterprise, API/DB/security changes | Full workflow + arch-review + test-plan |
|
|
67
|
+
Setup dw in project directory:
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
workflow:
|
|
91
|
-
default_depth: "standard"
|
|
69
|
+
```bash
|
|
70
|
+
dw init
|
|
92
71
|
```
|
|
93
72
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
## Kiến Trúc v1 (4 Layers)
|
|
73
|
+
Then in **Claude Code CLI**, run the full workflow:
|
|
99
74
|
|
|
100
75
|
```
|
|
101
|
-
|
|
102
|
-
Layer 1: .claude/ ← Claude Code execution (agents, hooks, skills)
|
|
103
|
-
Layer 2: config/claude: ← Model-specific features (extended thinking, MCP)
|
|
104
|
-
Layer 3: adapters/overrides/ ← Team customizations (never overwritten by upgrade)
|
|
76
|
+
/dw-flow your-task-or-anythings
|
|
105
77
|
```
|
|
106
78
|
|
|
107
79
|
---
|
|
108
80
|
|
|
109
|
-
|
|
81
|
+
Discover other skills:
|
|
110
82
|
|
|
111
83
|
```
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
│ ├── WORKFLOW.md ← 6-phase workflow
|
|
116
|
-
│ ├── THINKING.md ← thinking framework
|
|
117
|
-
│ ├── QUALITY.md ← 4-layer quality strategy
|
|
118
|
-
│ └── ROLES.md ← team role definitions
|
|
119
|
-
├── config/
|
|
120
|
-
│ ├── dw.config.yml ← config (~45 lines)
|
|
121
|
-
│ ├── config.schema.json ← validation schema
|
|
122
|
-
│ └── presets/ ← solo-quick, small-team, enterprise
|
|
123
|
-
├── adapters/
|
|
124
|
-
│ ├── claude-cli/overrides/ ← team customizations (upgrade-safe)
|
|
125
|
-
│ ├── claude-cli/extensions/ ← net-new team skills
|
|
126
|
-
│ └── generic/AGENT.md ← for Cursor/Windsurf/Copilot
|
|
127
|
-
├── .claude/ ← skills, agents, rules, hooks
|
|
128
|
-
├── .dw/ ← tasks, docs, metrics, reports
|
|
129
|
-
└── scripts/
|
|
130
|
-
├── upgrade.sh ← upgrade toolkit (--dry-run)
|
|
131
|
-
└── migrate-v03-to-v1.sh ← migration từ v0.3
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Skills Có Sẵn
|
|
84
|
+
/dw-prompt
|
|
85
|
+
/dw-thinking
|
|
86
|
+
...
|
|
137
87
|
|
|
138
|
-
|
|
88
|
+
```
|
|
139
89
|
|
|
140
90
|
---
|
|
141
91
|
|
|
142
|
-
##
|
|
92
|
+
## CLI commands
|
|
143
93
|
|
|
144
94
|
```bash
|
|
145
|
-
dw
|
|
146
|
-
dw
|
|
95
|
+
dw init # setup wizard / presets
|
|
96
|
+
dw validate # validate .dw/config/dw.config.yml
|
|
97
|
+
dw doctor # installation health check
|
|
98
|
+
dw upgrade # update toolkit files (override-aware)
|
|
99
|
+
dw upgrade --check # check for updates only
|
|
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)
|
|
147
104
|
```
|
|
148
105
|
|
|
149
|
-
|
|
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.
|
|
150
107
|
|
|
151
|
-
|
|
152
|
-
bash scripts/migrate-v03-to-v1.sh --dry-run
|
|
153
|
-
bash scripts/migrate-v03-to-v1.sh
|
|
154
|
-
```
|
|
108
|
+
---
|
|
155
109
|
|
|
156
|
-
|
|
110
|
+
## Depth system
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
- Map `level: 2` → `default_depth: standard`
|
|
160
|
-
- Preserve customized skills vào `.dw/adapters/claude-cli/overrides/`
|
|
161
|
-
- Backup old config, create new `.dw/config/dw.config.yml`
|
|
112
|
+
Pick a default depth for your project, then override per task when risk increases.
|
|
162
113
|
|
|
163
|
-
|
|
114
|
+
| Depth | Best for | Workflow |
|
|
115
|
+
|-------|----------|----------|
|
|
116
|
+
| `quick` | Solo dev, hotfix, familiar code | Understand → Execute → Close |
|
|
117
|
+
| `standard` | Small teams, new features | Full 6 phases |
|
|
118
|
+
| `thorough` | Risky changes (API/DB/security) | Full workflow + arch-review + test-plan |
|
|
164
119
|
|
|
165
|
-
|
|
120
|
+
Configured in `.dw/config/dw.config.yml`:
|
|
166
121
|
|
|
167
|
-
|
|
168
|
-
|
|
122
|
+
```yaml
|
|
123
|
+
workflow:
|
|
124
|
+
default_depth: "standard"
|
|
125
|
+
```
|
|
169
126
|
|
|
170
127
|
---
|
|
171
128
|
|
|
172
|
-
##
|
|
129
|
+
## What gets added to your repo?
|
|
173
130
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
| [CHANGELOG.md](CHANGELOG.md) | Lịch sử thay đổi |
|
|
131
|
+
```
|
|
132
|
+
.dw/ # methodology, config, adapters, task docs
|
|
133
|
+
.claude/ # Claude Code: skills, hooks, agents, rules
|
|
134
|
+
CLAUDE.md # project context for the agent
|
|
135
|
+
```
|
|
180
136
|
|
|
181
137
|
---
|
|
182
138
|
|
|
183
|
-
|
|
139
|
+
Maintainer: [huygdv](mailto:huygdv19@gmail.com)
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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": {
|
|
7
|
-
"dw": "
|
|
7
|
+
"dw": "bin/dw.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
".dw/adapters/",
|
|
15
15
|
"scripts/",
|
|
16
16
|
".claude/",
|
|
17
|
-
"CLAUDE.md"
|
|
18
|
-
"setup.sh"
|
|
17
|
+
"CLAUDE.md"
|
|
19
18
|
],
|
|
20
19
|
"engines": {
|
|
21
20
|
"node": ">=18"
|
|
@@ -40,13 +39,14 @@
|
|
|
40
39
|
"license": "MIT",
|
|
41
40
|
"repository": {
|
|
42
41
|
"type": "git",
|
|
43
|
-
"url": "https://github.com/dv-workflow/dv-workflow.git"
|
|
42
|
+
"url": "git+https://github.com/dv-workflow/dv-workflow.git"
|
|
44
43
|
},
|
|
45
44
|
"homepage": "https://github.com/dv-workflow/dv-workflow#readme",
|
|
46
45
|
"dependencies": {
|
|
47
46
|
"ajv": "^8.18.0",
|
|
48
47
|
"chalk": "^5.6.2",
|
|
49
48
|
"commander": "^14.0.3",
|
|
49
|
+
"enquirer": "^2.4.1",
|
|
50
50
|
"js-yaml": "^4.1.1"
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
#!/bin/bash
|
|
2
2
|
# scripts/e2e-local-check.sh
|
|
3
3
|
# End-to-end local publish check (pack -> install -> run CLI)
|
|
4
4
|
|
|
@@ -60,7 +60,6 @@ npx dw init --preset small-team
|
|
|
60
60
|
npx dw validate
|
|
61
61
|
npx dw doctor
|
|
62
62
|
npx dw upgrade --check
|
|
63
|
-
npx dw migrate --dry-run
|
|
64
63
|
|
|
65
64
|
info "Step 6: Verify task-depth override guidance artifacts"
|
|
66
65
|
grep -q "Task-Level Depth Override" ".dw/core/WORKFLOW.md"
|
|
@@ -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
|
|
@@ -56,13 +62,31 @@ export function run(argv) {
|
|
|
56
62
|
});
|
|
57
63
|
|
|
58
64
|
program
|
|
59
|
-
.command('
|
|
60
|
-
.description('
|
|
61
|
-
.option('-
|
|
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')
|
|
62
79
|
.action(async (opts) => {
|
|
63
|
-
const {
|
|
64
|
-
await
|
|
80
|
+
const { claudeVnFixCommand } = await import('./commands/claude-vn-fix.mjs');
|
|
81
|
+
await claudeVnFixCommand(opts);
|
|
65
82
|
});
|
|
66
83
|
|
|
67
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
|
+
}
|
|
68
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
|
+
}
|