elliot-stack 1.0.29 → 1.0.33
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/LICENSE +21 -21
- package/README.md +5 -0
- package/bin/install.cjs +981 -950
- package/hooks/repo-search-nudge.js +32 -32
- package/package.json +1 -1
- package/skills/estack-active-learning-tutor/SKILL.md +339 -339
- package/skills/estack-better-title/SKILL.md +64 -64
- package/skills/estack-better-title/scripts/rename.sh +55 -55
- package/skills/estack-chris-voss/SKILL.md +80 -80
- package/skills/estack-chris-voss/references/elliot-notes.md +120 -120
- package/skills/estack-chris-voss/references/voss-principles.md +210 -210
- package/skills/estack-customer-discovery/SKILL.md +60 -60
- package/skills/estack-flight-planner/SKILL.md +332 -332
- package/skills/estack-flight-planner/references/config_schema.md +156 -156
- package/skills/estack-flight-planner/references/flight_history_schema.md +97 -97
- package/skills/estack-flight-planner/references/shuttle_schedules.md +98 -98
- package/skills/estack-flight-planner/scripts/check_setup.sh +89 -89
- package/skills/estack-flight-planner/scripts/fetch_flights.py +99 -99
- package/skills/estack-flight-planner/scripts/filter_flights.py +265 -265
- package/skills/estack-flight-planner/scripts/pair_shuttles.py +173 -173
- package/skills/estack-github-issue-tracker/SKILL.md +322 -322
- package/skills/estack-github-issue-tracker/bin/tracker-tools.cjs +1358 -1358
- package/skills/estack-github-issue-tracker/references/gh-cli-patterns.md +124 -124
- package/skills/estack-github-issue-tracker/references/result-file-schema.md +156 -156
- package/skills/estack-github-issue-tracker/references/tracker-schema.md +96 -96
- package/skills/estack-github-issue-tracker/tracker-template.md +58 -58
- package/skills/estack-leadership-coach/SKILL.md +235 -0
- package/skills/estack-leadership-coach/adding-references.md +280 -0
- package/skills/estack-leadership-coach/frameworks/delegation/flows/post-mortem.md +120 -0
- package/skills/estack-leadership-coach/frameworks/delegation/flows/pre-delegation.md +138 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/1-intake.md +145 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/2-trm-assessment.md +119 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/3-enrollment.md +132 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/4-build-brief.md +171 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/5-monitoring.md +134 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/6-reverse-delegation.md +118 -0
- package/skills/estack-leadership-coach/frameworks/delegation/phases/7-diagnose.md +200 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__deci-olafsen-ryan-2017-self-determination-theory-in-work-organizations.md +1881 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__gagne-deci-2005-self-determination-theory-and-work-motivation.md +2058 -0
- package/skills/estack-leadership-coach/references/.source-files/deci-ryan_self-determination-theory__selfdeterminationtheory-org-theory-overview-page.md +61 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-3-key-insights-into-the-global-workplace-2024.md +57 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-managers-account-for-70-percent-of-variance-in-employee-engagement-2015.md +40 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-state-of-the-global-workplace-2026-global-data-summary.md +73 -0
- package/skills/estack-leadership-coach/references/.source-files/gallup_engagement-research__gallup-state-of-the-global-workplace-2026-report-landing.md +42 -0
- package/skills/estack-leadership-coach/references/.source-files/hormozi-leila_4-stages__leila-hormozi-the-art-of-delegation-blog-post.md +91 -0
- package/skills/estack-leadership-coach/references/.source-files/oncken-wass_monkeys-hbr-1974__oncken-wass-management-time-whos-got-the-monkey-hbr-classic-1974.md +969 -0
- package/skills/estack-leadership-coach/references/.source-files/sanchez_main-street-millionaire__codie-sanchez-afford-anything-podcast-ep-565-show-notes.md +89 -0
- package/skills/estack-leadership-coach/references/.source-files/sullivan_who-not-how__dan-sullivan-impact-filter-tool-and-guide-booklet.md +565 -0
- package/skills/estack-leadership-coach/references/.source-files/van-edwards_cues__vanessa-van-edwards-lewis-howes-school-of-greatness-ep-1231-show-notes.md +122 -0
- package/skills/estack-leadership-coach/references/.source-files/van-edwards_cues__vanessa-van-edwards-roger-dooley-cues-interview.md +194 -0
- package/skills/estack-leadership-coach/references/deci-ryan_self-determination-theory.md +166 -0
- package/skills/estack-leadership-coach/references/doerr_measure-what-matters.md +154 -0
- package/skills/estack-leadership-coach/references/ferriss_4hww.md +189 -0
- package/skills/estack-leadership-coach/references/gallup_engagement-research.md +105 -0
- package/skills/estack-leadership-coach/references/gerber_e-myth-revisited.md +118 -0
- package/skills/estack-leadership-coach/references/grove_high-output-management.md +95 -0
- package/skills/estack-leadership-coach/references/hormozi-alex_followthrough.md +152 -0
- package/skills/estack-leadership-coach/references/hormozi-leila_4-stages.md +146 -0
- package/skills/estack-leadership-coach/references/oncken-wass_monkeys-hbr-1974.md +128 -0
- package/skills/estack-leadership-coach/references/sanchez_main-street-millionaire.md +196 -0
- package/skills/estack-leadership-coach/references/sullivan_who-not-how.md +137 -0
- package/skills/estack-leadership-coach/references/van-edwards_cues.md +189 -0
- package/skills/estack-migrate-claude-session-history/SKILL.md +226 -0
- package/skills/estack-migrate-claude-session-history/references/path-encoding.md +55 -0
- package/skills/estack-migrate-claude-session-history/references/troubleshooting.md +96 -0
- package/skills/estack-migrate-claude-session-history/scripts/migrate-claude-history.js +1123 -0
- package/skills/estack-migrate-claude-session-history/scripts/test-append-note.js +48 -0
- package/skills/estack-migrate-claude-session-history/scripts/test-validate-migration.py +326 -0
- package/skills/estack-migrate-claude-session-history/scripts/validate-migration.py +493 -0
- package/skills/estack-pdf-to-md/SKILL.md +180 -0
- package/skills/estack-pdf-to-md/scripts/pdf_to_md.py +596 -0
- package/skills/estack-productivity-prioritization-coach/SKILL.md +124 -0
- package/skills/estack-productivity-prioritization-coach/sources/01-tony-robbins-rpm.md +39 -0
- package/skills/estack-productivity-prioritization-coach/sources/02-justin-sung-task-prioritization.md +34 -0
- package/skills/estack-prompt-builder-coach/SKILL.md +81 -81
- package/skills/estack-prompt-builder-coach/definition-of-done-generator.md +42 -42
- package/skills/estack-prompt-builder-coach/prompt-builder.md +37 -37
- package/skills/estack-prompt-builder-coach/task-shaper.md +36 -36
- package/skills/estack-prompt-builder-coach/vague-ask-auditor.md +37 -37
- package/skills/estack-read-claude-session-history/SKILL.md +204 -204
- package/skills/estack-read-claude-session-history/references/jsonl-schema.md +126 -126
- package/skills/estack-read-claude-session-history/references/modes.md +423 -423
- package/skills/estack-read-claude-session-history/references/recipes.md +271 -271
- package/skills/estack-read-claude-session-history/scripts/lib/__init__.py +1 -1
- package/skills/estack-read-claude-session-history/scripts/lib/parser.py +460 -460
- package/skills/estack-read-claude-session-history/scripts/lib/paths.py +234 -234
- package/skills/estack-read-claude-session-history/scripts/lib/search.py +179 -179
- package/skills/estack-read-claude-session-history/scripts/lib/subagents.py +88 -88
- package/skills/estack-read-claude-session-history/scripts/lib/tools.py +144 -144
- package/skills/estack-read-claude-session-history/scripts/read_transcript.py +1776 -1776
- package/skills/estack-read-claude-session-history/scripts/tests/conftest.py +40 -40
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/README.md +20 -20
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/all-noise.jsonl +4 -4
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/basic-session.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-gaps.jsonl +9 -9
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-noise.jsonl +7 -7
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-a.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-parallel-b.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/engagement-waiting.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/interrupted.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/multi-compact.jsonl +8 -8
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/pending-user.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta/subagents/agent-aaa.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-no-meta.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent/subagents/agent-xyz123.meta.json +1 -1
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/subagent-parent.jsonl +4 -4
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/time-spread.jsonl +6 -6
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/timeline-day-test.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/tool-zoo.jsonl +10 -10
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/truncated.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/unicode.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-advisor.jsonl +3 -3
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-compact.jsonl +5 -5
- package/skills/estack-read-claude-session-history/scripts/tests/fixtures/with-thinking.jsonl +2 -2
- package/skills/estack-read-claude-session-history/scripts/tests/test_backup_roots.py +56 -56
- package/skills/estack-read-claude-session-history/scripts/tests/test_engagement.py +239 -239
- package/skills/estack-read-claude-session-history/scripts/tests/test_json_format.py +201 -201
- package/skills/estack-read-claude-session-history/scripts/tests/test_modes.py +199 -199
- package/skills/estack-read-claude-session-history/scripts/tests/test_parser.py +195 -195
- package/skills/estack-read-claude-session-history/scripts/tests/test_paths.py +133 -133
- package/skills/estack-read-claude-session-history/scripts/tests/test_search.py +78 -78
- package/skills/estack-read-claude-session-history/scripts/tests/test_subagents.py +43 -43
- package/skills/estack-read-claude-session-history/scripts/tests/test_timeline.py +179 -179
- package/skills/estack-read-claude-session-history/scripts/tests/test_timezone_and_project.py +212 -212
- package/skills/estack-read-claude-session-history/scripts/tests/test_tools.py +80 -80
- package/skills/estack-repo-search/SKILL.md +65 -65
- package/skills/estack-vscode-file-recovery/SKILL.md +188 -0
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* =============================================================================
|
|
10
|
+
* CLAUDE CODE CHAT HISTORY MIGRATION SCRIPT
|
|
11
|
+
* =============================================================================
|
|
12
|
+
*
|
|
13
|
+
* PURPOSE:
|
|
14
|
+
* When you rename or move a local repo folder, Claude Code loses track of
|
|
15
|
+
* your chat history because it keys project data on the folder path. This
|
|
16
|
+
* script migrates all chat history from the old project directory to the new
|
|
17
|
+
* one, rewriting every internal path reference so Claude Code treats the
|
|
18
|
+
* history as belonging to the new location.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT DOES:
|
|
21
|
+
* - Copies all session files (.jsonl), subagent logs, tool results, and
|
|
22
|
+
* memory files from the old Claude Code project dir to the new one
|
|
23
|
+
* - Finds and replaces 9 path encoding variants (JSON-escaped backslash,
|
|
24
|
+
* forward slash, MSYS/Git Bash, hyphenated project dir name, plain
|
|
25
|
+
* backslash — each in upper/lowercase drive letter variants)
|
|
26
|
+
* - Merges sessions-index.json entries (if one exists)
|
|
27
|
+
* - Runs a post-migration verification scan for any missed references
|
|
28
|
+
* - Never overwrites files that already exist in the new project dir
|
|
29
|
+
*
|
|
30
|
+
* FULL PROCESS (do these steps in order):
|
|
31
|
+
*
|
|
32
|
+
* 1. RENAME ON GITHUB (if applicable):
|
|
33
|
+
* - Go to repo Settings > General > rename, or use:
|
|
34
|
+
* gh repo rename new-name --yes
|
|
35
|
+
*
|
|
36
|
+
* 2. RENAME LOCAL FOLDER(S):
|
|
37
|
+
* - Rename the repo folder (and parent if needed) to the new name
|
|
38
|
+
* - This is just a folder rename — git history, branches, uncommitted
|
|
39
|
+
* changes, stashes all survive because .git/ is inside the folder
|
|
40
|
+
*
|
|
41
|
+
* 3. UPDATE GIT REMOTE URL:
|
|
42
|
+
* - cd into the renamed folder and run:
|
|
43
|
+
* git remote set-url origin https://github.com/OWNER/NEW-REPO-NAME.git
|
|
44
|
+
*
|
|
45
|
+
* 4. START A NEW CLAUDE CODE SESSION IN THE NEW FOLDER:
|
|
46
|
+
* - cd into the renamed folder and run: claude
|
|
47
|
+
* - Say anything, then exit. This makes Claude Code create the new
|
|
48
|
+
* project directory structure under ~/.claude/projects/
|
|
49
|
+
* - This step is important — the script needs the new project dir to
|
|
50
|
+
* already exist so it merges into the right place
|
|
51
|
+
*
|
|
52
|
+
* 5. CONFIGURE AND RUN THIS SCRIPT:
|
|
53
|
+
* - Set OLD_REPO_PATH to the ORIGINAL folder path (before rename)
|
|
54
|
+
* - Set NEW_REPO_PATH to the NEW folder path (after rename)
|
|
55
|
+
* - Optionally set CLAUDE_DIR if your .claude dir is non-standard
|
|
56
|
+
* - Run: node migrate-claude-history.js --dry-run (preview first)
|
|
57
|
+
* - Run: node migrate-claude-history.js (execute)
|
|
58
|
+
*
|
|
59
|
+
* OR override via CLI flags (no constant edits needed):
|
|
60
|
+
* node migrate-claude-history.js \
|
|
61
|
+
* --old-repo "C:\path\to\old" \
|
|
62
|
+
* --new-repo "C:\path\to\new" \
|
|
63
|
+
* [--session <uuid>] \
|
|
64
|
+
* [--dry-run]
|
|
65
|
+
*
|
|
66
|
+
* SINGLE-SESSION MODE (--session <uuid-or-filename>):
|
|
67
|
+
* Migrate just one .jsonl session file instead of the entire project
|
|
68
|
+
* directory. Useful when you want to move a single conversation to a
|
|
69
|
+
* different project (e.g. it logically belongs to a sub-project).
|
|
70
|
+
* Skips sessions-index merging; only verifies the one transformed file.
|
|
71
|
+
* The UUID can be the bare id ("db151ec9-...") or the full filename
|
|
72
|
+
* ("db151ec9-...jsonl").
|
|
73
|
+
*
|
|
74
|
+
* 6. VERIFY:
|
|
75
|
+
* - Open Claude Code in the new folder
|
|
76
|
+
* - Run /resume — your old chat history should appear
|
|
77
|
+
* - The script prints a verification report; 0 stale refs = clean
|
|
78
|
+
* - NOTE: "stale reference" warnings for the project name appearing in
|
|
79
|
+
* conversation TEXT (not metadata) are false positives and harmless
|
|
80
|
+
*
|
|
81
|
+
* 7. CLEANUP (optional):
|
|
82
|
+
* - Delete the old project dir: ~/.claude/projects/<old-project-dir>
|
|
83
|
+
* - Delete any backup you made
|
|
84
|
+
*
|
|
85
|
+
* PATH ENCODING VARIANTS (auto-generated from the two paths you provide):
|
|
86
|
+
* A) C:\\Users\\name\\old\\repo -> C:\\Users\\name\\new\\repo (JSON-escaped backslash, uppercase drive)
|
|
87
|
+
* B) c:\\Users\\name\\old\\repo -> c:\\Users\\name\\new\\repo (JSON-escaped backslash, lowercase drive)
|
|
88
|
+
* C) C:/Users/name/old/repo -> C:/Users/name/new/repo (forward slash, uppercase)
|
|
89
|
+
* D) c--Users-name-old-repo -> c--Users-name-new-repo (project dir name, lowercase)
|
|
90
|
+
* E) /c/Users/name/old/repo -> /c/Users/name/new/repo (MSYS/Git Bash path)
|
|
91
|
+
* F) C--Users-name-old-repo -> C--Users-name-new-repo (project dir name, uppercase)
|
|
92
|
+
* G) c:/Users/name/old/repo -> c:/Users/name/new/repo (forward slash, lowercase)
|
|
93
|
+
* H) C:\Users\name\old\repo -> C:\Users\name\new\repo (plain backslash, uppercase)
|
|
94
|
+
* I) c:\Users\name\old\repo -> c:\Users\name\new\repo (plain backslash, lowercase)
|
|
95
|
+
*
|
|
96
|
+
* Patterns are sorted longest-first to prevent partial matches.
|
|
97
|
+
*
|
|
98
|
+
* REQUIREMENTS:
|
|
99
|
+
* - Node.js (no external dependencies)
|
|
100
|
+
* - Windows (paths are Windows-specific; the script validates this)
|
|
101
|
+
*
|
|
102
|
+
* =============================================================================
|
|
103
|
+
*
|
|
104
|
+
* CONFIGURATION — edit these three values:
|
|
105
|
+
*/
|
|
106
|
+
const OLD_REPO_PATH = String.raw`C:\Users\2supe\All Coding\Elliot's Skills\elliot-skills`;
|
|
107
|
+
const NEW_REPO_PATH = String.raw`C:\Users\2supe\All Coding\E-Stack\e-stack`;
|
|
108
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
109
|
+
|
|
110
|
+
const HELP_TEXT = `
|
|
111
|
+
Usage:
|
|
112
|
+
node migrate-claude-history.js [options]
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--old-repo <path> Override OLD_REPO_PATH constant (absolute Windows path)
|
|
116
|
+
--new-repo <path> Override NEW_REPO_PATH constant (absolute Windows path)
|
|
117
|
+
--session <id> Single-session mode: migrate only one .jsonl file
|
|
118
|
+
(UUID with or without ".jsonl" suffix). Skips
|
|
119
|
+
sessions-index merging.
|
|
120
|
+
--dry-run Show what would happen without writing any files
|
|
121
|
+
--no-migration-note Do NOT append a "this session was migrated" note to
|
|
122
|
+
each migrated .jsonl. STRONGLY DISCOURAGED. The note
|
|
123
|
+
tells future-you (and Claude on /resume) that file
|
|
124
|
+
paths in the transcript were rewritten and may not
|
|
125
|
+
exist on disk. Without it, you can waste real time
|
|
126
|
+
debugging missing-file errors that are expected
|
|
127
|
+
side-effects of the rewrite. Only pass this if you
|
|
128
|
+
actually moved every file the transcript references
|
|
129
|
+
to the new location.
|
|
130
|
+
--help, -h Show this help
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
# Migrate entire project (uses OLD_REPO_PATH/NEW_REPO_PATH constants)
|
|
134
|
+
node migrate-claude-history.js --dry-run
|
|
135
|
+
node migrate-claude-history.js
|
|
136
|
+
|
|
137
|
+
# Migrate a single session to a different project, with CLI overrides
|
|
138
|
+
node migrate-claude-history.js \\
|
|
139
|
+
--old-repo "C:\\Users\\me\\Projects\\workspace" \\
|
|
140
|
+
--new-repo "C:\\Users\\me\\Projects\\workspace\\sub-project" \\
|
|
141
|
+
--session db151ec9-eae7-422e-be9c-3591f012e45b \\
|
|
142
|
+
--dry-run
|
|
143
|
+
`.trim();
|
|
144
|
+
|
|
145
|
+
function main(argv = process.argv.slice(2)) {
|
|
146
|
+
const args = parseArgs(argv);
|
|
147
|
+
|
|
148
|
+
if (args.help) {
|
|
149
|
+
console.log(HELP_TEXT);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const config = buildConfig({
|
|
154
|
+
oldRepoPath: args.oldRepo || OLD_REPO_PATH,
|
|
155
|
+
newRepoPath: args.newRepo || NEW_REPO_PATH,
|
|
156
|
+
claudeDir: CLAUDE_DIR,
|
|
157
|
+
dryRun: args.dryRun,
|
|
158
|
+
sessionFilter: args.session,
|
|
159
|
+
appendMigrationNote: !args.noMigrationNote,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
runMigration(config);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseArgs(argv) {
|
|
166
|
+
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
167
|
+
const flagsWithValues = new Set(['--old-repo', '--new-repo', '--session']);
|
|
168
|
+
const knownBooleanFlags = new Set(['--dry-run', '--help', '-h', '--no-migration-note']);
|
|
169
|
+
|
|
170
|
+
const result = {
|
|
171
|
+
dryRun: false,
|
|
172
|
+
help: false,
|
|
173
|
+
oldRepo: null,
|
|
174
|
+
newRepo: null,
|
|
175
|
+
session: null,
|
|
176
|
+
noMigrationNote: false,
|
|
177
|
+
};
|
|
178
|
+
const unknownArgs = [];
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
181
|
+
const arg = args[i];
|
|
182
|
+
|
|
183
|
+
if (arg === '--dry-run') {
|
|
184
|
+
result.dryRun = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (arg === '--no-migration-note') {
|
|
189
|
+
result.noMigrationNote = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (arg === '--help' || arg === '-h') {
|
|
194
|
+
result.help = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle --flag=value form
|
|
199
|
+
const eqIndex = arg.indexOf('=');
|
|
200
|
+
if (eqIndex > 0) {
|
|
201
|
+
const flagName = arg.slice(0, eqIndex);
|
|
202
|
+
const flagValue = arg.slice(eqIndex + 1);
|
|
203
|
+
if (flagsWithValues.has(flagName)) {
|
|
204
|
+
assignFlagValue(result, flagName, flagValue);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle --flag value form
|
|
210
|
+
if (flagsWithValues.has(arg)) {
|
|
211
|
+
const nextValue = args[i + 1];
|
|
212
|
+
if (typeof nextValue !== 'string' || knownBooleanFlags.has(nextValue) || flagsWithValues.has(nextValue)) {
|
|
213
|
+
throw new Error(`Flag ${arg} requires a value.\n\n${HELP_TEXT}`);
|
|
214
|
+
}
|
|
215
|
+
assignFlagValue(result, arg, nextValue);
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
unknownArgs.push(arg);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (unknownArgs.length > 0) {
|
|
224
|
+
throw new Error(`Unknown argument(s): ${unknownArgs.join(', ')}\n\n${HELP_TEXT}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assignFlagValue(result, flagName, value) {
|
|
231
|
+
if (flagName === '--old-repo') result.oldRepo = value;
|
|
232
|
+
else if (flagName === '--new-repo') result.newRepo = value;
|
|
233
|
+
else if (flagName === '--session') result.session = value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function buildConfig(options) {
|
|
237
|
+
const oldRepo = parseWindowsRepoPath(options.oldRepoPath, 'OLD_REPO_PATH');
|
|
238
|
+
const newRepo = parseWindowsRepoPath(options.newRepoPath, 'NEW_REPO_PATH');
|
|
239
|
+
|
|
240
|
+
if (oldRepo.normalized.toLowerCase() === newRepo.normalized.toLowerCase()) {
|
|
241
|
+
throw new Error('OLD_REPO_PATH and NEW_REPO_PATH must be different.');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const claudeDir = path.resolve(expandHomePath(options.claudeDir));
|
|
245
|
+
const projectsRoot = path.join(claudeDir, 'projects');
|
|
246
|
+
const oldProjectNames = buildProjectDirNames(oldRepo);
|
|
247
|
+
const newProjectNames = buildProjectDirNames(newRepo);
|
|
248
|
+
const replacementPairs = buildReplacementPairs(oldRepo, newRepo);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
dryRun: Boolean(options.dryRun),
|
|
252
|
+
claudeDir,
|
|
253
|
+
projectsRoot,
|
|
254
|
+
oldRepo,
|
|
255
|
+
newRepo,
|
|
256
|
+
oldProjectNames,
|
|
257
|
+
newProjectNames,
|
|
258
|
+
replacementPairs,
|
|
259
|
+
sessionFilter: normalizeSessionFilter(options.sessionFilter),
|
|
260
|
+
appendMigrationNote: options.appendMigrationNote !== false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeSessionFilter(value) {
|
|
265
|
+
if (value === null || value === undefined) return null;
|
|
266
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
267
|
+
throw new Error('--session must be a non-empty UUID or filename.');
|
|
268
|
+
}
|
|
269
|
+
const trimmed = value.trim();
|
|
270
|
+
return trimmed.toLowerCase().endsWith('.jsonl') ? trimmed : `${trimmed}.jsonl`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function runMigration(config, deps = {}) {
|
|
274
|
+
const activeFs = deps.fs || fs;
|
|
275
|
+
|
|
276
|
+
if (!activeFs.existsSync(config.projectsRoot)) {
|
|
277
|
+
throw new Error(`Claude projects directory does not exist: ${config.projectsRoot}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const oldProjectDir = resolveExistingOldProjectDir(
|
|
281
|
+
config.projectsRoot,
|
|
282
|
+
config.oldProjectNames,
|
|
283
|
+
activeFs,
|
|
284
|
+
);
|
|
285
|
+
const newProjectDir = resolveNewProjectDir(
|
|
286
|
+
config.projectsRoot,
|
|
287
|
+
config.newProjectNames,
|
|
288
|
+
activeFs,
|
|
289
|
+
);
|
|
290
|
+
const summary = createSummary(config.replacementPairs, config.dryRun);
|
|
291
|
+
|
|
292
|
+
if (!activeFs.statSync(oldProjectDir).isDirectory()) {
|
|
293
|
+
throw new Error(`Old Claude project directory is not a directory: ${oldProjectDir}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
printHeader(config, oldProjectDir, newProjectDir, activeFs);
|
|
297
|
+
ensureDirectory(newProjectDir, config.dryRun, summary, activeFs);
|
|
298
|
+
|
|
299
|
+
let filesToCopy = collectFiles(oldProjectDir, activeFs)
|
|
300
|
+
.filter((filePath) => path.basename(filePath) !== 'sessions-index.json');
|
|
301
|
+
|
|
302
|
+
if (config.sessionFilter) {
|
|
303
|
+
// sessionFilter is "<uuid>.jsonl". The matching sidecar directory is
|
|
304
|
+
// "<uuid>/" (subagent transcripts, etc.) — include all files under it.
|
|
305
|
+
const sessionId = config.sessionFilter.replace(/\.jsonl$/i, '');
|
|
306
|
+
const sidecarDir = path.join(oldProjectDir, sessionId);
|
|
307
|
+
const sidecarPrefix = sidecarDir + path.sep;
|
|
308
|
+
const matched = filesToCopy.filter((filePath) => (
|
|
309
|
+
path.basename(filePath) === config.sessionFilter
|
|
310
|
+
|| filePath === sidecarDir
|
|
311
|
+
|| filePath.startsWith(sidecarPrefix)
|
|
312
|
+
));
|
|
313
|
+
if (matched.length === 0) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Single-session mode: file "${config.sessionFilter}" not found in old project dir.\n ${oldProjectDir}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const sidecarCount = matched.length - matched.filter((p) => path.basename(p) === config.sessionFilter).length;
|
|
319
|
+
filesToCopy = matched;
|
|
320
|
+
console.log(`Single-session mode: migrating ${config.sessionFilter}` + (sidecarCount > 0 ? ` + ${sidecarCount} sidecar file(s)` : ''));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const copiedDestinations = [];
|
|
324
|
+
for (const sourcePath of filesToCopy) {
|
|
325
|
+
const relativePath = path.relative(oldProjectDir, sourcePath);
|
|
326
|
+
const destinationPath = path.join(newProjectDir, relativePath);
|
|
327
|
+
|
|
328
|
+
if (activeFs.existsSync(destinationPath)) {
|
|
329
|
+
summary.filesSkipped += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
copyAndTransformFile(
|
|
334
|
+
sourcePath,
|
|
335
|
+
destinationPath,
|
|
336
|
+
config.replacementPairs,
|
|
337
|
+
config.dryRun,
|
|
338
|
+
summary,
|
|
339
|
+
activeFs,
|
|
340
|
+
);
|
|
341
|
+
summary.filesCopied += 1;
|
|
342
|
+
copiedDestinations.push(destinationPath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!config.sessionFilter) {
|
|
346
|
+
mergeSessionsIndex({
|
|
347
|
+
oldProjectDir,
|
|
348
|
+
newProjectDir,
|
|
349
|
+
replacementPairs: config.replacementPairs,
|
|
350
|
+
newRepo: config.newRepo,
|
|
351
|
+
dryRun: config.dryRun,
|
|
352
|
+
summary,
|
|
353
|
+
fs: activeFs,
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
console.log('Single-session mode: skipping sessions-index.json merge.');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Append a migration note to every top-level session .jsonl we just wrote.
|
|
360
|
+
// This is on by default (and strongly recommended). Subagent sidecar
|
|
361
|
+
// .jsonl files are intentionally excluded — Claude only reads the main
|
|
362
|
+
// session file on /resume, so that's the only place the note matters.
|
|
363
|
+
if (config.appendMigrationNote) {
|
|
364
|
+
const sessionJsonlDestinations = copiedDestinations.filter(
|
|
365
|
+
(destPath) => path.dirname(destPath) === newProjectDir && destPath.toLowerCase().endsWith('.jsonl'),
|
|
366
|
+
);
|
|
367
|
+
for (const destPath of sessionJsonlDestinations) {
|
|
368
|
+
appendMigrationNote({
|
|
369
|
+
filePath: destPath,
|
|
370
|
+
oldRepo: config.oldRepo,
|
|
371
|
+
newRepo: config.newRepo,
|
|
372
|
+
dryRun: config.dryRun,
|
|
373
|
+
summary,
|
|
374
|
+
fs: activeFs,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
console.log('--no-migration-note set: skipping migration-note append (NOT RECOMMENDED).');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
printSummary(summary, config.replacementPairs);
|
|
382
|
+
|
|
383
|
+
// Post-migration verification: check for any remaining old path references.
|
|
384
|
+
// In single-session mode, only check the file(s) we just wrote; otherwise
|
|
385
|
+
// scan the whole new project dir.
|
|
386
|
+
const verifyRoot = config.sessionFilter
|
|
387
|
+
? { type: 'files', paths: copiedDestinations }
|
|
388
|
+
: { type: 'dir', path: newProjectDir };
|
|
389
|
+
const staleCount = verifyNoStaleReferences(verifyRoot, config.oldRepo, activeFs);
|
|
390
|
+
if (staleCount > 0) {
|
|
391
|
+
console.log('');
|
|
392
|
+
console.log(`WARNING: Found ${staleCount} file(s) in the new project dir that still contain old path references.`);
|
|
393
|
+
console.log('These may need manual review.');
|
|
394
|
+
} else if (!config.dryRun) {
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log('Verification: no remaining old path references found in migrated files.');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
config,
|
|
401
|
+
oldProjectDir,
|
|
402
|
+
newProjectDir,
|
|
403
|
+
summary,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function createSummary(replacementPairs, dryRun) {
|
|
408
|
+
return {
|
|
409
|
+
dryRun,
|
|
410
|
+
filesCopied: 0,
|
|
411
|
+
filesSkipped: 0,
|
|
412
|
+
indexEntriesMerged: 0,
|
|
413
|
+
indexEntriesSkipped: 0,
|
|
414
|
+
migrationNotesAppended: 0,
|
|
415
|
+
migrationNotesSkipped: 0,
|
|
416
|
+
createdDirectories: new Set(),
|
|
417
|
+
replacementsByPattern: Object.fromEntries(
|
|
418
|
+
replacementPairs.map((pair) => [pair.label, 0]),
|
|
419
|
+
),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function resolveNewProjectDir(projectsRoot, projectNames, activeFs = fs) {
|
|
424
|
+
// Check both casings — use whichever already exists, preferring uppercase
|
|
425
|
+
// (Claude Code on Windows creates uppercase C-- dirs)
|
|
426
|
+
const upperDir = path.join(projectsRoot, projectNames.upper);
|
|
427
|
+
const lowerDir = path.join(projectsRoot, projectNames.lower);
|
|
428
|
+
|
|
429
|
+
if (activeFs.existsSync(upperDir) && activeFs.statSync(upperDir).isDirectory()) {
|
|
430
|
+
return upperDir;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (activeFs.existsSync(lowerDir) && activeFs.statSync(lowerDir).isDirectory()) {
|
|
434
|
+
return lowerDir;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Neither exists — default to uppercase (matches Claude Code's convention on Windows)
|
|
438
|
+
return upperDir;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function printHeader(config, oldProjectDir, newProjectDir, activeFs) {
|
|
442
|
+
console.log(config.dryRun ? 'DRY RUN: no files will be written.' : 'Migrating Claude history...');
|
|
443
|
+
console.log(`Old repo path: ${config.oldRepo.normalized}`);
|
|
444
|
+
console.log(`New repo path: ${config.newRepo.normalized}`);
|
|
445
|
+
console.log(`Claude dir: ${config.claudeDir}`);
|
|
446
|
+
console.log(`Old project: ${oldProjectDir}`);
|
|
447
|
+
console.log(`New project: ${newProjectDir}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseWindowsRepoPath(input, variableName) {
|
|
451
|
+
if (typeof input !== 'string' || input.trim() === '') {
|
|
452
|
+
throw new Error(`${variableName} must be a non-empty absolute Windows path.`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const normalized = stripTrailingSlashes(path.win32.normalize(input.trim()));
|
|
456
|
+
const match = /^([A-Za-z]):[\\/](.+)$/.exec(normalized);
|
|
457
|
+
|
|
458
|
+
if (!match) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`${variableName} must be an absolute Windows path like C:\\Users\\name\\repo. Received: ${input}`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const drive = match[1];
|
|
465
|
+
const segments = match[2].split(/[\\/]+/).filter(Boolean);
|
|
466
|
+
|
|
467
|
+
if (segments.length === 0) {
|
|
468
|
+
throw new Error(`${variableName} must point to a project directory, not a drive root.`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
normalized: `${drive.toUpperCase()}:\\${segments.join('\\')}`,
|
|
473
|
+
driveUpper: drive.toUpperCase(),
|
|
474
|
+
driveLower: drive.toLowerCase(),
|
|
475
|
+
segments,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function expandHomePath(inputPath) {
|
|
480
|
+
if (typeof inputPath !== 'string' || inputPath.trim() === '') {
|
|
481
|
+
throw new Error('CLAUDE_DIR must be a non-empty path.');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (inputPath === '~') {
|
|
485
|
+
return os.homedir();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (inputPath.startsWith(`~${path.sep}`) || inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
|
|
489
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return inputPath;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function stripTrailingSlashes(input) {
|
|
496
|
+
return input.replace(/[\\/]+$/, '');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildProjectDirNames(repoPath) {
|
|
500
|
+
return {
|
|
501
|
+
lower: toHyphenatedProjectDirName(repoPath, 'lower'),
|
|
502
|
+
upper: toHyphenatedProjectDirName(repoPath, 'upper'),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function toWindowsPath(repoPath, driveCase) {
|
|
507
|
+
const drive = driveCase === 'lower' ? repoPath.driveLower : repoPath.driveUpper;
|
|
508
|
+
return `${drive}:\\${repoPath.segments.join('\\')}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function toJsonEscapedBackslashPath(repoPath, driveCase) {
|
|
512
|
+
const drive = driveCase === 'lower' ? repoPath.driveLower : repoPath.driveUpper;
|
|
513
|
+
return `${drive}:\\\\${repoPath.segments.join('\\\\')}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function toForwardSlashPath(repoPath, driveCase) {
|
|
517
|
+
const drive = driveCase === 'lower' ? repoPath.driveLower : repoPath.driveUpper;
|
|
518
|
+
return `${drive}:/${repoPath.segments.join('/')}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function toMsysPath(repoPath) {
|
|
522
|
+
return `/${repoPath.driveLower}/${repoPath.segments.join('/')}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function toHyphenatedProjectDirName(repoPath, driveCase) {
|
|
526
|
+
return toWindowsPath(repoPath, driveCase).replace(/:/g, '-').replace(/[\\/ ']/g, '-');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function buildReplacementPairs(oldRepo, newRepo) {
|
|
530
|
+
const pairs = [
|
|
531
|
+
{
|
|
532
|
+
key: 'A',
|
|
533
|
+
description: 'Windows backslash uppercase drive, JSON-escaped',
|
|
534
|
+
oldValue: toJsonEscapedBackslashPath(oldRepo, 'upper'),
|
|
535
|
+
newValue: toJsonEscapedBackslashPath(newRepo, 'upper'),
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
key: 'B',
|
|
539
|
+
description: 'Windows backslash lowercase drive, JSON-escaped',
|
|
540
|
+
oldValue: toJsonEscapedBackslashPath(oldRepo, 'lower'),
|
|
541
|
+
newValue: toJsonEscapedBackslashPath(newRepo, 'lower'),
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
key: 'C',
|
|
545
|
+
description: 'Forward slash uppercase drive',
|
|
546
|
+
oldValue: toForwardSlashPath(oldRepo, 'upper'),
|
|
547
|
+
newValue: toForwardSlashPath(newRepo, 'upper'),
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
key: 'D',
|
|
551
|
+
description: 'Project directory name lowercase drive',
|
|
552
|
+
oldValue: toHyphenatedProjectDirName(oldRepo, 'lower'),
|
|
553
|
+
newValue: toHyphenatedProjectDirName(newRepo, 'lower'),
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
key: 'E',
|
|
557
|
+
description: 'MSYS/Git Bash path',
|
|
558
|
+
oldValue: toMsysPath(oldRepo),
|
|
559
|
+
newValue: toMsysPath(newRepo),
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
key: 'F',
|
|
563
|
+
description: 'Project directory name uppercase drive',
|
|
564
|
+
oldValue: toHyphenatedProjectDirName(oldRepo, 'upper'),
|
|
565
|
+
newValue: toHyphenatedProjectDirName(newRepo, 'upper'),
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
key: 'G',
|
|
569
|
+
description: 'Forward slash lowercase drive',
|
|
570
|
+
oldValue: toForwardSlashPath(oldRepo, 'lower'),
|
|
571
|
+
newValue: toForwardSlashPath(newRepo, 'lower'),
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
key: 'H',
|
|
575
|
+
description: 'Windows backslash uppercase drive (plain)',
|
|
576
|
+
oldValue: toWindowsPath(oldRepo, 'upper'),
|
|
577
|
+
newValue: toWindowsPath(newRepo, 'upper'),
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
key: 'I',
|
|
581
|
+
description: 'Windows backslash lowercase drive (plain)',
|
|
582
|
+
oldValue: toWindowsPath(oldRepo, 'lower'),
|
|
583
|
+
newValue: toWindowsPath(newRepo, 'lower'),
|
|
584
|
+
},
|
|
585
|
+
];
|
|
586
|
+
|
|
587
|
+
return pairs
|
|
588
|
+
.slice()
|
|
589
|
+
.sort((left, right) => right.oldValue.length - left.oldValue.length)
|
|
590
|
+
.map((pair) => ({
|
|
591
|
+
...pair,
|
|
592
|
+
label: `${pair.key}) ${pair.description}`,
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function resolveExistingOldProjectDir(projectsRoot, projectNames, activeFs = fs) {
|
|
597
|
+
const candidates = [
|
|
598
|
+
path.join(projectsRoot, projectNames.lower),
|
|
599
|
+
path.join(projectsRoot, projectNames.upper),
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
for (const candidate of candidates) {
|
|
603
|
+
if (activeFs.existsSync(candidate) && activeFs.statSync(candidate).isDirectory()) {
|
|
604
|
+
return candidate;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
throw new Error(
|
|
609
|
+
`Could not find old Claude project directory.\nChecked:\n- ${candidates.join('\n- ')}`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function collectFiles(rootDir, activeFs = fs) {
|
|
614
|
+
const files = [];
|
|
615
|
+
const queue = [rootDir];
|
|
616
|
+
|
|
617
|
+
while (queue.length > 0) {
|
|
618
|
+
const currentDir = queue.pop();
|
|
619
|
+
const entries = activeFs.readdirSync(currentDir, { withFileTypes: true });
|
|
620
|
+
|
|
621
|
+
for (const entry of entries) {
|
|
622
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
623
|
+
|
|
624
|
+
if (entry.isDirectory()) {
|
|
625
|
+
queue.push(fullPath);
|
|
626
|
+
} else if (entry.isFile()) {
|
|
627
|
+
files.push(fullPath);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return files;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function ensureDirectory(directoryPath, dryRun, summary, activeFs = fs) {
|
|
636
|
+
if (activeFs.existsSync(directoryPath)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!dryRun) {
|
|
641
|
+
activeFs.mkdirSync(directoryPath, { recursive: true });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
summary.createdDirectories.add(directoryPath);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function copyAndTransformFile(
|
|
648
|
+
sourcePath,
|
|
649
|
+
destinationPath,
|
|
650
|
+
replacementPairs,
|
|
651
|
+
dryRun,
|
|
652
|
+
summary,
|
|
653
|
+
activeFs = fs,
|
|
654
|
+
) {
|
|
655
|
+
ensureDirectory(path.dirname(destinationPath), dryRun, summary, activeFs);
|
|
656
|
+
|
|
657
|
+
const sourceBuffer = activeFs.readFileSync(sourcePath);
|
|
658
|
+
|
|
659
|
+
if (!isProbablyTextFile(sourceBuffer)) {
|
|
660
|
+
if (!dryRun) {
|
|
661
|
+
activeFs.writeFileSync(destinationPath, sourceBuffer);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const sourceText = sourceBuffer.toString('utf8');
|
|
667
|
+
const transformed = applyReplacementPairs(sourceText, replacementPairs);
|
|
668
|
+
addReplacementCounts(summary, transformed.counts);
|
|
669
|
+
|
|
670
|
+
if (!dryRun) {
|
|
671
|
+
activeFs.writeFileSync(destinationPath, transformed.text, 'utf8');
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function isProbablyTextFile(buffer) {
|
|
676
|
+
return buffer.indexOf(0) === -1;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function applyReplacementPairs(text, replacementPairs) {
|
|
680
|
+
let nextText = text;
|
|
681
|
+
const counts = Object.fromEntries(replacementPairs.map((pair) => [pair.label, 0]));
|
|
682
|
+
|
|
683
|
+
for (const pair of replacementPairs) {
|
|
684
|
+
const result = replaceAllLiteral(nextText, pair.oldValue, pair.newValue);
|
|
685
|
+
nextText = result.text;
|
|
686
|
+
counts[pair.label] += result.count;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return { text: nextText, counts };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function replaceAllLiteral(text, search, replacement) {
|
|
693
|
+
if (search === '') {
|
|
694
|
+
return { text, count: 0 };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let count = 0;
|
|
698
|
+
let cursor = 0;
|
|
699
|
+
let output = '';
|
|
700
|
+
|
|
701
|
+
while (true) {
|
|
702
|
+
const index = text.indexOf(search, cursor);
|
|
703
|
+
|
|
704
|
+
if (index === -1) {
|
|
705
|
+
output += text.slice(cursor);
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
output += text.slice(cursor, index);
|
|
710
|
+
output += replacement;
|
|
711
|
+
cursor = index + search.length;
|
|
712
|
+
count += 1;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return { text: output, count };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function addReplacementCounts(summary, counts) {
|
|
719
|
+
for (const [label, count] of Object.entries(counts)) {
|
|
720
|
+
summary.replacementsByPattern[label] += count;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function mergeSessionsIndex(options) {
|
|
725
|
+
const {
|
|
726
|
+
oldProjectDir,
|
|
727
|
+
newProjectDir,
|
|
728
|
+
replacementPairs,
|
|
729
|
+
newRepo,
|
|
730
|
+
dryRun,
|
|
731
|
+
summary,
|
|
732
|
+
fs: activeFs = fs,
|
|
733
|
+
} = options;
|
|
734
|
+
|
|
735
|
+
const oldIndexPath = path.join(oldProjectDir, 'sessions-index.json');
|
|
736
|
+
const newIndexPath = path.join(newProjectDir, 'sessions-index.json');
|
|
737
|
+
|
|
738
|
+
if (!activeFs.existsSync(oldIndexPath)) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const oldIndex = normalizeSessionsIndexShape(readJsonFile(oldIndexPath, activeFs), oldIndexPath);
|
|
743
|
+
const newIndex = activeFs.existsSync(newIndexPath)
|
|
744
|
+
? normalizeSessionsIndexShape(readJsonFile(newIndexPath, activeFs), newIndexPath)
|
|
745
|
+
: { version: numberOrDefault(oldIndex.version, 1), entries: [] };
|
|
746
|
+
|
|
747
|
+
const mergedEntries = newIndex.entries.slice();
|
|
748
|
+
const seenKeys = new Set();
|
|
749
|
+
|
|
750
|
+
for (const entry of mergedEntries) {
|
|
751
|
+
addIndexEntryKeys(seenKeys, entry);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for (const entry of oldIndex.entries) {
|
|
755
|
+
const transformed = transformSessionsIndexEntry(entry, replacementPairs, newRepo);
|
|
756
|
+
addReplacementCounts(summary, transformed.counts);
|
|
757
|
+
|
|
758
|
+
const entryKeys = getIndexEntryKeys(transformed.entry);
|
|
759
|
+
const isDuplicate = entryKeys.some((key) => seenKeys.has(key));
|
|
760
|
+
|
|
761
|
+
if (isDuplicate) {
|
|
762
|
+
summary.indexEntriesSkipped += 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
mergedEntries.push(transformed.entry);
|
|
767
|
+
entryKeys.forEach((key) => seenKeys.add(key));
|
|
768
|
+
summary.indexEntriesMerged += 1;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const mergedIndex = {
|
|
772
|
+
...newIndex,
|
|
773
|
+
version: Math.max(numberOrDefault(newIndex.version, 1), numberOrDefault(oldIndex.version, 1)),
|
|
774
|
+
entries: mergedEntries,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Update originalPath at root level if it references the old path
|
|
778
|
+
if (typeof mergedIndex.originalPath === 'string') {
|
|
779
|
+
const transformedOriginal = applyReplacementPairs(mergedIndex.originalPath, replacementPairs);
|
|
780
|
+
mergedIndex.originalPath = transformedOriginal.text;
|
|
781
|
+
addReplacementCounts(summary, transformedOriginal.counts);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (!dryRun) {
|
|
785
|
+
activeFs.writeFileSync(newIndexPath, `${JSON.stringify(mergedIndex, null, 2)}\n`, 'utf8');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function readJsonFile(filePath, activeFs = fs) {
|
|
790
|
+
try {
|
|
791
|
+
return JSON.parse(activeFs.readFileSync(filePath, 'utf8'));
|
|
792
|
+
} catch (error) {
|
|
793
|
+
throw new Error(`Failed to parse JSON at ${filePath}: ${error.message}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function normalizeSessionsIndexShape(value, filePath) {
|
|
798
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
799
|
+
throw new Error(`Unexpected sessions-index.json shape at ${filePath}: expected an object.`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (!Array.isArray(value.entries)) {
|
|
803
|
+
throw new Error(`Unexpected sessions-index.json shape at ${filePath}: missing "entries" array.`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function transformSessionsIndexEntry(entry, replacementPairs, newRepo) {
|
|
810
|
+
const clone = entry && typeof entry === 'object'
|
|
811
|
+
? JSON.parse(JSON.stringify(entry))
|
|
812
|
+
: {};
|
|
813
|
+
const totalCounts = Object.fromEntries(replacementPairs.map((pair) => [pair.label, 0]));
|
|
814
|
+
|
|
815
|
+
if (typeof clone.fullPath === 'string') {
|
|
816
|
+
const transformedFullPath = applyReplacementPairs(clone.fullPath, replacementPairs);
|
|
817
|
+
clone.fullPath = transformedFullPath.text;
|
|
818
|
+
accumulateCounts(totalCounts, transformedFullPath.counts);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (typeof clone.projectPath === 'string' && clone.projectPath.trim() !== '') {
|
|
822
|
+
const rewrittenProjectPath = rewriteProjectPath(clone.projectPath, replacementPairs, newRepo);
|
|
823
|
+
clone.projectPath = rewrittenProjectPath.text;
|
|
824
|
+
accumulateCounts(totalCounts, rewrittenProjectPath.counts);
|
|
825
|
+
} else {
|
|
826
|
+
clone.projectPath = toWindowsPath(newRepo, 'upper');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Transform all other string fields (firstPrompt, name, summary, etc.)
|
|
830
|
+
// that may contain old path references
|
|
831
|
+
const handledFields = new Set(['fullPath', 'projectPath']);
|
|
832
|
+
for (const [key, value] of Object.entries(clone)) {
|
|
833
|
+
if (handledFields.has(key) || typeof value !== 'string') continue;
|
|
834
|
+
const transformed = applyReplacementPairs(value, replacementPairs);
|
|
835
|
+
if (transformed.text !== value) {
|
|
836
|
+
clone[key] = transformed.text;
|
|
837
|
+
accumulateCounts(totalCounts, transformed.counts);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return { entry: clone, counts: totalCounts };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function rewriteProjectPath(currentValue, replacementPairs, newRepo) {
|
|
845
|
+
const transformed = applyReplacementPairs(currentValue, replacementPairs);
|
|
846
|
+
|
|
847
|
+
if (transformed.text !== currentValue) {
|
|
848
|
+
return transformed;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
let fallbackText;
|
|
852
|
+
|
|
853
|
+
if (/^\/[A-Za-z]\//.test(currentValue)) {
|
|
854
|
+
fallbackText = toMsysPath(newRepo);
|
|
855
|
+
} else if (/^[a-z]:\//.test(currentValue)) {
|
|
856
|
+
fallbackText = toForwardSlashPath(newRepo, 'lower');
|
|
857
|
+
} else if (/^[A-Z]:\//.test(currentValue)) {
|
|
858
|
+
fallbackText = toForwardSlashPath(newRepo, 'upper');
|
|
859
|
+
} else if (/^[a-z]:\\/.test(currentValue)) {
|
|
860
|
+
fallbackText = toWindowsPath(newRepo, 'lower');
|
|
861
|
+
} else {
|
|
862
|
+
fallbackText = toWindowsPath(newRepo, 'upper');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
text: fallbackText,
|
|
867
|
+
counts: transformed.counts,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function accumulateCounts(target, source) {
|
|
872
|
+
for (const [label, count] of Object.entries(source)) {
|
|
873
|
+
target[label] += count;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function addIndexEntryKeys(targetSet, entry) {
|
|
878
|
+
for (const key of getIndexEntryKeys(entry)) {
|
|
879
|
+
targetSet.add(key);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function getIndexEntryKeys(entry) {
|
|
884
|
+
const keys = [];
|
|
885
|
+
|
|
886
|
+
if (entry && typeof entry === 'object') {
|
|
887
|
+
if (typeof entry.sessionId === 'string' && entry.sessionId !== '') {
|
|
888
|
+
keys.push(`sessionId:${entry.sessionId}`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (typeof entry.fullPath === 'string' && entry.fullPath !== '') {
|
|
892
|
+
keys.push(`fullPath:${entry.fullPath}`);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return keys;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function numberOrDefault(value, fallback) {
|
|
900
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function appendMigrationNote({ filePath, oldRepo, newRepo, dryRun, summary, fs: activeFs = fs }) {
|
|
904
|
+
if (!activeFs.existsSync(filePath)) {
|
|
905
|
+
if (dryRun) {
|
|
906
|
+
console.log(` [dry-run] would append migration note to ${path.basename(filePath)}`);
|
|
907
|
+
if (summary) summary.migrationNotesAppended += 1;
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const existingText = activeFs.readFileSync(filePath, 'utf8');
|
|
913
|
+
const lines = existingText.split(/\r?\n/).filter((line) => line.trim() !== '');
|
|
914
|
+
|
|
915
|
+
if (lines.length === 0) {
|
|
916
|
+
if (summary) summary.migrationNotesSkipped += 1;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Don't double-append if a migration note is already the last user entry.
|
|
921
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
922
|
+
let entry;
|
|
923
|
+
try {
|
|
924
|
+
entry = JSON.parse(lines[i]);
|
|
925
|
+
} catch (_) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const content = entry && entry.message && entry.message.content;
|
|
929
|
+
if (typeof content === 'string' && content.includes('<session-migration-note>')) {
|
|
930
|
+
if (summary) summary.migrationNotesSkipped += 1;
|
|
931
|
+
console.log(` Migration note already present in ${path.basename(filePath)} — not re-appending.`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
// Only check the last few entries — bail if we go too far back
|
|
935
|
+
if (lines.length - i > 5) break;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Find the last entry that has a uuid so we can chain off it.
|
|
939
|
+
let lastWithUuid = null;
|
|
940
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
941
|
+
try {
|
|
942
|
+
const entry = JSON.parse(lines[i]);
|
|
943
|
+
if (entry && typeof entry.uuid === 'string' && entry.uuid !== '') {
|
|
944
|
+
lastWithUuid = entry;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
} catch (_) {
|
|
948
|
+
// skip unparseable
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!lastWithUuid) {
|
|
953
|
+
if (summary) summary.migrationNotesSkipped += 1;
|
|
954
|
+
console.log(` No uuid-bearing entry found in ${path.basename(filePath)} — skipping migration note.`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const sessionId = lastWithUuid.sessionId || path.basename(filePath, '.jsonl');
|
|
959
|
+
const nowIso = new Date().toISOString();
|
|
960
|
+
const oldPath = toWindowsPath(oldRepo, 'upper');
|
|
961
|
+
const newPath = toWindowsPath(newRepo, 'upper');
|
|
962
|
+
|
|
963
|
+
const noteText = [
|
|
964
|
+
'<session-migration-note>',
|
|
965
|
+
`This session was originally recorded in project ${oldPath} and was`,
|
|
966
|
+
`migrated on ${nowIso} to project ${newPath}.`,
|
|
967
|
+
'',
|
|
968
|
+
'During the migration, every occurrence of the old project path was',
|
|
969
|
+
'rewritten to the new project path throughout this transcript (cwd',
|
|
970
|
+
'fields, tool inputs, tool outputs, and conversation text). The rewrite',
|
|
971
|
+
'is correct for paths whose files now live at the new location. For',
|
|
972
|
+
'paths whose files actually still live at the old location, the rewrite',
|
|
973
|
+
'produced a reference that does not exist on disk.',
|
|
974
|
+
'',
|
|
975
|
+
'If you hit a missing-file error while resuming this session, try the',
|
|
976
|
+
`original path (${oldPath}\\...) instead of the rewritten path`,
|
|
977
|
+
`(${newPath}\\...), or search the parent directory for the filename.`,
|
|
978
|
+
'</session-migration-note>',
|
|
979
|
+
].join('\n');
|
|
980
|
+
|
|
981
|
+
const newEntry = {
|
|
982
|
+
parentUuid: lastWithUuid.uuid,
|
|
983
|
+
isSidechain: false,
|
|
984
|
+
promptId: crypto.randomUUID(),
|
|
985
|
+
type: 'user',
|
|
986
|
+
message: { role: 'user', content: noteText },
|
|
987
|
+
uuid: crypto.randomUUID(),
|
|
988
|
+
timestamp: nowIso,
|
|
989
|
+
userType: 'external',
|
|
990
|
+
entrypoint: 'cli',
|
|
991
|
+
cwd: newPath,
|
|
992
|
+
sessionId,
|
|
993
|
+
version: lastWithUuid.version || '',
|
|
994
|
+
gitBranch: lastWithUuid.gitBranch || '',
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
if (dryRun) {
|
|
998
|
+
console.log(` [dry-run] would append migration note to ${path.basename(filePath)}`);
|
|
999
|
+
if (summary) summary.migrationNotesAppended += 1;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Append on a new line; preserve trailing newline shape
|
|
1004
|
+
const separator = existingText.endsWith('\n') ? '' : '\n';
|
|
1005
|
+
activeFs.appendFileSync(filePath, separator + JSON.stringify(newEntry) + '\n', 'utf8');
|
|
1006
|
+
if (summary) summary.migrationNotesAppended += 1;
|
|
1007
|
+
console.log(` Appended migration note to ${path.basename(filePath)}`);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function verifyNoStaleReferences(scope, oldRepo, activeFs = fs) {
|
|
1011
|
+
const searchStrings = [
|
|
1012
|
+
toJsonEscapedBackslashPath(oldRepo, 'upper'),
|
|
1013
|
+
toJsonEscapedBackslashPath(oldRepo, 'lower'),
|
|
1014
|
+
toWindowsPath(oldRepo, 'upper'),
|
|
1015
|
+
toWindowsPath(oldRepo, 'lower'),
|
|
1016
|
+
toForwardSlashPath(oldRepo, 'upper'),
|
|
1017
|
+
toForwardSlashPath(oldRepo, 'lower'),
|
|
1018
|
+
toMsysPath(oldRepo),
|
|
1019
|
+
toHyphenatedProjectDirName(oldRepo, 'lower'),
|
|
1020
|
+
toHyphenatedProjectDirName(oldRepo, 'upper'),
|
|
1021
|
+
];
|
|
1022
|
+
|
|
1023
|
+
let files;
|
|
1024
|
+
let baseDir;
|
|
1025
|
+
if (scope && scope.type === 'files') {
|
|
1026
|
+
files = scope.paths.filter((p) => activeFs.existsSync(p));
|
|
1027
|
+
baseDir = null;
|
|
1028
|
+
} else if (scope && scope.type === 'dir') {
|
|
1029
|
+
files = collectFiles(scope.path, activeFs);
|
|
1030
|
+
baseDir = scope.path;
|
|
1031
|
+
} else {
|
|
1032
|
+
// Back-compat: caller passed a raw directory path
|
|
1033
|
+
files = collectFiles(scope, activeFs);
|
|
1034
|
+
baseDir = scope;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
let staleFileCount = 0;
|
|
1038
|
+
|
|
1039
|
+
for (const filePath of files) {
|
|
1040
|
+
const buffer = activeFs.readFileSync(filePath);
|
|
1041
|
+
if (!isProbablyTextFile(buffer)) continue;
|
|
1042
|
+
|
|
1043
|
+
const text = buffer.toString('utf8');
|
|
1044
|
+
for (const search of searchStrings) {
|
|
1045
|
+
if (text.includes(search)) {
|
|
1046
|
+
const displayPath = baseDir ? path.relative(baseDir, filePath) : filePath;
|
|
1047
|
+
console.log(` Stale reference in: ${displayPath} (matches: ${search.slice(0, 40)}...)`);
|
|
1048
|
+
staleFileCount += 1;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return staleFileCount;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function printSummary(summary, replacementPairs) {
|
|
1058
|
+
console.log('');
|
|
1059
|
+
console.log('Summary');
|
|
1060
|
+
console.log(`Files copied: ${summary.filesCopied}`);
|
|
1061
|
+
console.log(`Files skipped (already exist): ${summary.filesSkipped}`);
|
|
1062
|
+
console.log(`sessions-index entries merged: ${summary.indexEntriesMerged}`);
|
|
1063
|
+
console.log(`sessions-index entries skipped: ${summary.indexEntriesSkipped}`);
|
|
1064
|
+
console.log(`Migration notes appended: ${summary.migrationNotesAppended}`);
|
|
1065
|
+
if (summary.migrationNotesSkipped > 0) {
|
|
1066
|
+
console.log(`Migration notes skipped (already present / no anchor): ${summary.migrationNotesSkipped}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (summary.createdDirectories.size > 0) {
|
|
1070
|
+
console.log(`Directories created: ${summary.createdDirectories.size}`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
console.log('Replacements by pattern:');
|
|
1074
|
+
for (const pair of replacementPairs) {
|
|
1075
|
+
console.log(` ${pair.label}: ${summary.replacementsByPattern[pair.label]}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
module.exports = {
|
|
1080
|
+
HELP_TEXT,
|
|
1081
|
+
OLD_REPO_PATH,
|
|
1082
|
+
NEW_REPO_PATH,
|
|
1083
|
+
CLAUDE_DIR,
|
|
1084
|
+
main,
|
|
1085
|
+
parseArgs,
|
|
1086
|
+
buildConfig,
|
|
1087
|
+
runMigration,
|
|
1088
|
+
parseWindowsRepoPath,
|
|
1089
|
+
expandHomePath,
|
|
1090
|
+
buildProjectDirNames,
|
|
1091
|
+
toWindowsPath,
|
|
1092
|
+
toJsonEscapedBackslashPath,
|
|
1093
|
+
toForwardSlashPath,
|
|
1094
|
+
toMsysPath,
|
|
1095
|
+
toHyphenatedProjectDirName,
|
|
1096
|
+
buildReplacementPairs,
|
|
1097
|
+
resolveExistingOldProjectDir,
|
|
1098
|
+
resolveNewProjectDir,
|
|
1099
|
+
collectFiles,
|
|
1100
|
+
ensureDirectory,
|
|
1101
|
+
copyAndTransformFile,
|
|
1102
|
+
isProbablyTextFile,
|
|
1103
|
+
applyReplacementPairs,
|
|
1104
|
+
replaceAllLiteral,
|
|
1105
|
+
mergeSessionsIndex,
|
|
1106
|
+
readJsonFile,
|
|
1107
|
+
normalizeSessionsIndexShape,
|
|
1108
|
+
transformSessionsIndexEntry,
|
|
1109
|
+
rewriteProjectPath,
|
|
1110
|
+
getIndexEntryKeys,
|
|
1111
|
+
numberOrDefault,
|
|
1112
|
+
verifyNoStaleReferences,
|
|
1113
|
+
appendMigrationNote,
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
if (require.main === module) {
|
|
1117
|
+
try {
|
|
1118
|
+
main();
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
console.error(`Error: ${error.message}`);
|
|
1121
|
+
process.exitCode = 1;
|
|
1122
|
+
}
|
|
1123
|
+
}
|