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/README.md CHANGED
@@ -1,183 +1,139 @@
1
- # dw-kit
1
+ # dw-kit
2
2
 
3
- > Bộ workflow toolkit cho dev team sử dụng Claude Code Agenttừ requirements đến dashboard.
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) · [Cheatsheet](docs/cheatsheet.md) · [Migration v0.3→v1](scripts/migrate-v03-to-v1.sh)
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
- ## Toolkit Này Làm Gì?
9
+ ## What is dw-kit?
10
10
 
11
- Thay để Claude tự do code, dw-kit tạo ra **rails cấu trúc**:
11
+ dw-kit helps your team run AI-assisted development with a **repeatable workflow** and clear checkpoints:
12
12
 
13
13
  ```
14
- Research Plan Execute (TDD) Review Commit
14
+ Initialize Understand → Plan Execute (TDD) Verify Close
15
15
  ```
16
16
 
17
- Với đầy đủ hỗ trợ cho các roles trong team: BA · TL · Dev · QC · PM.
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
- ## Quick Start
26
+ E --> V["Verify (gates)"]
27
+ V -->|sign-off| C["Close (handoff + archive)"]
28
+ V -->|"revise (fix)"| E
22
29
 
23
- ### Option A — npm (recommended)
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
- ```bash
26
- npm install -g dw-kit
36
+ class R,Est,AR,TP,DU,LW extra
27
37
  ```
28
38
 
29
- Then in your project directory:
39
+ ## Workflow overview
30
40
 
31
- ```bash
32
- dw init
33
- ```
41
+ `dw` runs a 6-phase process (all phases for `standard` and `thorough`):
34
42
 
35
- Interactive wizard asks 3 questions (project, depth, language) and auto-selects roles by depth. Or use presets:
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
- ```bash
38
- dw init --preset small-team # skip wizard
39
- dw init --preset solo-quick # solo dev, minimal ceremony
40
- dw init --preset enterprise # full team, all features
41
- ```
42
-
43
- Zero-install (one-time use):
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
- ### Option B Git submodule (legacy)
53
+ It’s designed for collaboration (Dev / Tech Lead / QA / PM) and keeps work auditable via lightweight task docs.
50
54
 
51
- ```bash
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
- ### CLI Commands
57
+ ## Install
67
58
 
68
59
  ```bash
69
- dw init # Setup wizard
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
- ## Depth System (thay thế Level 1/2/3)
65
+ ## Quick start
81
66
 
82
- | Depth | Dành cho | Workflow |
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
- Cấu hình trong `.dw/config/dw.config.yml`:
89
- ```yaml
90
- workflow:
91
- default_depth: "standard"
69
+ ```bash
70
+ dw init
92
71
  ```
93
72
 
94
- `default_depth` baseline. Với task cụ thể, bạn có thể override sang `thorough` khi scope/risk tăng (API/DB/security), kể cả project nhỏ.
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
- Layer 0: core/ Portable methodology (platform-agnostic)
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
- ## Cấu Trúc Sau Khi Setup
81
+ Discover other skills:
110
82
 
111
83
  ```
112
- dự-án-của-bạn/
113
- ├── .dw-module/ ← toolkit (git submodule, read-only)
114
- ├── core/ ← portable methodology
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
- Xem [docs/cheatsheet.md](docs/cheatsheet.md) để có bảng tham chiếu nhanh.
88
+ ```
139
89
 
140
90
  ---
141
91
 
142
- ## Migrating từ v0.3
92
+ ## CLI commands
143
93
 
144
94
  ```bash
145
- dw migrate --dry-run # preview changes
146
- dw migrate # apply migration
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
- Or via bash (legacy):
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
- ```bash
152
- bash scripts/migrate-v03-to-v1.sh --dry-run
153
- bash scripts/migrate-v03-to-v1.sh
154
- ```
108
+ ---
155
109
 
156
- `scripts/upgrade.sh` `scripts/migrate-v03-to-v1.sh` được giữ cho backward-compat; ưu tiên dùng `dw upgrade` và `dw migrate`.
110
+ ## Depth system
157
111
 
158
- Migration sẽ:
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
- ## Demo
120
+ Configured in `.dw/config/dw.config.yml`:
166
121
 
167
- - [Demo A](examples/demo-A-bug-fix/) — Bug fix workflow (quick depth)
168
- - [Demo B](examples/demo-B-new-feature/) — Full team feature workflow (BA → PM)
122
+ ```yaml
123
+ workflow:
124
+ default_depth: "standard"
125
+ ```
169
126
 
170
127
  ---
171
128
 
172
- ## Tài Liệu
129
+ ## What gets added to your repo?
173
130
 
174
- | Tài liệu | Nội dung |
175
- |----------|---------|
176
- | [docs/README.md](docs/README.md) | Hướng dẫn đầy đủ, setup, tips |
177
- | [docs/cheatsheet.md](docs/cheatsheet.md) | Bảng tham chiếu nhanh tất cả skills |
178
- | [docs/custom-skills.md](docs/custom-skills.md) | Hướng dẫn tạo custom skills |
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
- > Maintainer: [huygdv](mailto:huygdv19@gmail.com)
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.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": "./bin/dw.mjs"
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
- #!/bin/bash
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('migrate')
60
- .description('Migrate project from dw-kit v0.3 to v1')
61
- .option('-n, --dry-run', 'Preview migration without applying')
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 { migrateCommand } = await import('./commands/migrate.mjs');
64
- await migrateCommand(opts);
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
+ }