@vibe-x/agent-better-checkpoint 0.3.2 → 0.3.4
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/bin/cli.mjs +233 -6
- package/package.json +8 -2
- package/platform/unix/alloc_patch.sh +122 -0
- package/platform/unix/check_uncommitted.sh +22 -5
- package/platform/unix/checkpoint.sh +103 -16
- package/platform/win/alloc_patch.ps1 +142 -0
- package/platform/win/check_uncommitted.ps1 +18 -3
- package/platform/win/checkpoint.ps1 +101 -18
- package/scripts/release-check.mjs +143 -0
- package/skill/SKILL.md +36 -14
package/bin/cli.mjs
CHANGED
|
@@ -38,7 +38,7 @@ const CONFIG_TEMPLATE = join(PLATFORM_DIR, 'config.template.yml');
|
|
|
38
38
|
// ============================================================
|
|
39
39
|
|
|
40
40
|
function parseArgs(argv) {
|
|
41
|
-
const args = { platform: null, uninstall: false, target: null };
|
|
41
|
+
const args = { platform: null, uninstall: false, target: null, activate: false };
|
|
42
42
|
for (let i = 2; i < argv.length; i++) {
|
|
43
43
|
switch (argv[i]) {
|
|
44
44
|
case '--platform':
|
|
@@ -58,6 +58,9 @@ function parseArgs(argv) {
|
|
|
58
58
|
case '--uninstall':
|
|
59
59
|
args.uninstall = true;
|
|
60
60
|
break;
|
|
61
|
+
case '--activate':
|
|
62
|
+
args.activate = true;
|
|
63
|
+
break;
|
|
61
64
|
case '--help':
|
|
62
65
|
case '-h':
|
|
63
66
|
printHelp();
|
|
@@ -82,6 +85,7 @@ Usage:
|
|
|
82
85
|
Options:
|
|
83
86
|
--platform <cursor|claude> Target AI platform (auto-detected if omitted)
|
|
84
87
|
--target <path> Project-only install (no global). Use . for cwd
|
|
88
|
+
--activate Activate checkpoint rules in current project (AGENTS.md only)
|
|
85
89
|
--uninstall Remove installed files and hook registrations
|
|
86
90
|
-h, --help Show this help message
|
|
87
91
|
`);
|
|
@@ -142,6 +146,80 @@ function writeJsonFile(filepath, data) {
|
|
|
142
146
|
writeFileSync(filepath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
// ============================================================
|
|
150
|
+
// AGENTS.md block management
|
|
151
|
+
// ============================================================
|
|
152
|
+
|
|
153
|
+
const AGENTS_BLOCK_START = '<!-- [ABC:agent-better-checkpoint:start] -->';
|
|
154
|
+
const AGENTS_BLOCK_END = '<!-- [ABC:agent-better-checkpoint:end] -->';
|
|
155
|
+
|
|
156
|
+
const AGENTS_BLOCK_CONTENT = `${AGENTS_BLOCK_START}
|
|
157
|
+
## Checkpoint Commit Rule
|
|
158
|
+
|
|
159
|
+
After completing any file edit (write_to_file, replace_in_file, multi_replace_in_file, or similar),
|
|
160
|
+
you MUST:
|
|
161
|
+
|
|
162
|
+
1. Load the "agent-better-checkpoint" skill (if not already loaded)
|
|
163
|
+
2. Follow its instructions to create a checkpoint commit
|
|
164
|
+
3. Do NOT proceed to the next task until the commit is done
|
|
165
|
+
|
|
166
|
+
This ensures every meaningful code change is tracked with a semantic Git commit.
|
|
167
|
+
${AGENTS_BLOCK_END}`;
|
|
168
|
+
|
|
169
|
+
function injectAgentsMdBlock(targetDir) {
|
|
170
|
+
const agentsMdPath = join(targetDir, 'AGENTS.md');
|
|
171
|
+
let content = '';
|
|
172
|
+
|
|
173
|
+
if (existsSync(agentsMdPath)) {
|
|
174
|
+
content = readFileSync(agentsMdPath, 'utf-8');
|
|
175
|
+
// Check if block already exists
|
|
176
|
+
if (content.includes(AGENTS_BLOCK_START)) {
|
|
177
|
+
console.log(` AGENTS.md → block already exists (skipped)`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Append block to end of file (or create new file)
|
|
183
|
+
const newContent = content
|
|
184
|
+
? content.trimEnd() + '\n\n' + AGENTS_BLOCK_CONTENT + '\n'
|
|
185
|
+
: AGENTS_BLOCK_CONTENT + '\n';
|
|
186
|
+
|
|
187
|
+
writeFileSync(agentsMdPath, newContent, 'utf-8');
|
|
188
|
+
console.log(` AGENTS.md → ${agentsMdPath}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeAgentsMdBlock(targetDir) {
|
|
192
|
+
const agentsMdPath = join(targetDir, 'AGENTS.md');
|
|
193
|
+
|
|
194
|
+
if (!existsSync(agentsMdPath)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
199
|
+
|
|
200
|
+
if (!content.includes(AGENTS_BLOCK_START)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Remove the block (including surrounding newlines)
|
|
205
|
+
const blockRegex = new RegExp(
|
|
206
|
+
`\\n*${AGENTS_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${AGENTS_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n*`,
|
|
207
|
+
'g'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
let newContent = content.replace(blockRegex, '\n');
|
|
211
|
+
newContent = newContent.trim();
|
|
212
|
+
|
|
213
|
+
if (newContent === '') {
|
|
214
|
+
// If file is empty after removal, delete it
|
|
215
|
+
rmSync(agentsMdPath, { force: true });
|
|
216
|
+
console.log(` Removed ${agentsMdPath} (empty after cleanup)`);
|
|
217
|
+
} else {
|
|
218
|
+
writeFileSync(agentsMdPath, newContent + '\n', 'utf-8');
|
|
219
|
+
console.log(` Cleaned ${agentsMdPath}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
145
223
|
// ============================================================
|
|
146
224
|
// Install logic
|
|
147
225
|
// ============================================================
|
|
@@ -155,11 +233,14 @@ function installScripts(osType) {
|
|
|
155
233
|
|
|
156
234
|
// 双端脚本都安装,方便跨平台使用
|
|
157
235
|
copyFileSafe(join(PLATFORM_DIR, 'unix', 'checkpoint.sh'), join(scriptsDir, 'checkpoint.sh'));
|
|
236
|
+
copyFileSafe(join(PLATFORM_DIR, 'unix', 'alloc_patch.sh'), join(scriptsDir, 'alloc_patch.sh'));
|
|
158
237
|
copyFileSafe(join(PLATFORM_DIR, 'unix', 'check_uncommitted.sh'), join(hooksDir, 'check_uncommitted.sh'));
|
|
159
238
|
setExecutable(join(scriptsDir, 'checkpoint.sh'));
|
|
239
|
+
setExecutable(join(scriptsDir, 'alloc_patch.sh'));
|
|
160
240
|
setExecutable(join(hooksDir, 'check_uncommitted.sh'));
|
|
161
241
|
|
|
162
242
|
copyFileSafe(join(PLATFORM_DIR, 'win', 'checkpoint.ps1'), join(scriptsDir, 'checkpoint.ps1'));
|
|
243
|
+
copyFileSafe(join(PLATFORM_DIR, 'win', 'alloc_patch.ps1'), join(scriptsDir, 'alloc_patch.ps1'));
|
|
163
244
|
copyFileSafe(join(PLATFORM_DIR, 'win', 'check_uncommitted.ps1'), join(hooksDir, 'check_uncommitted.ps1'));
|
|
164
245
|
|
|
165
246
|
console.log(` Scripts → ${scriptsDir}/`);
|
|
@@ -227,12 +308,18 @@ function registerCursorHook(osType) {
|
|
|
227
308
|
function installProjectOnly(targetDir, aiPlatform, osType) {
|
|
228
309
|
const root = resolve(targetDir);
|
|
229
310
|
|
|
230
|
-
// .vibe-x/agent-better-checkpoint: checkpoint 脚本 + config
|
|
311
|
+
// .vibe-x/agent-better-checkpoint: checkpoint 脚本 + helper + config
|
|
231
312
|
const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
|
|
232
313
|
ensureDir(vibeXBase);
|
|
233
314
|
copyFileSafe(join(PLATFORM_DIR, 'unix', 'checkpoint.sh'), join(vibeXBase, 'checkpoint.sh'));
|
|
315
|
+
copyFileSafe(join(PLATFORM_DIR, 'unix', 'alloc_patch.sh'), join(vibeXBase, 'alloc_patch.sh'));
|
|
316
|
+
copyFileSafe(join(PLATFORM_DIR, 'unix', 'check_uncommitted.sh'), join(vibeXBase, 'check_uncommitted.sh'));
|
|
234
317
|
copyFileSafe(join(PLATFORM_DIR, 'win', 'checkpoint.ps1'), join(vibeXBase, 'checkpoint.ps1'));
|
|
318
|
+
copyFileSafe(join(PLATFORM_DIR, 'win', 'alloc_patch.ps1'), join(vibeXBase, 'alloc_patch.ps1'));
|
|
319
|
+
copyFileSafe(join(PLATFORM_DIR, 'win', 'check_uncommitted.ps1'), join(vibeXBase, 'check_uncommitted.ps1'));
|
|
235
320
|
setExecutable(join(vibeXBase, 'checkpoint.sh'));
|
|
321
|
+
setExecutable(join(vibeXBase, 'alloc_patch.sh'));
|
|
322
|
+
setExecutable(join(vibeXBase, 'check_uncommitted.sh'));
|
|
236
323
|
const configDest = join(vibeXBase, 'config.yml');
|
|
237
324
|
if (!existsSync(configDest) && existsSync(CONFIG_TEMPLATE)) {
|
|
238
325
|
copyFileSafe(CONFIG_TEMPLATE, configDest);
|
|
@@ -271,6 +358,9 @@ function installProjectOnly(targetDir, aiPlatform, osType) {
|
|
|
271
358
|
// Claude Code: settings.json 为全局,无项目级 hooks,仅安装 skill 和脚本
|
|
272
359
|
console.log(` Hooks → (Claude stop hook is global-only, skipped for project install)`);
|
|
273
360
|
}
|
|
361
|
+
|
|
362
|
+
// Inject AGENTS.md block (project-only)
|
|
363
|
+
injectAgentsMdBlock(root);
|
|
274
364
|
}
|
|
275
365
|
|
|
276
366
|
function uninstallProjectOnly(targetDir, aiPlatform) {
|
|
@@ -279,9 +369,22 @@ function uninstallProjectOnly(targetDir, aiPlatform) {
|
|
|
279
369
|
const vibeXBase = join(root, '.vibe-x', 'agent-better-checkpoint');
|
|
280
370
|
const skillRoot = aiPlatform === 'cursor' ? '.cursor' : '.claude';
|
|
281
371
|
const skillDir = join(root, skillRoot, 'skills', SKILL_NAME);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
372
|
+
const checkpointShPath = join(vibeXBase, 'checkpoint.sh');
|
|
373
|
+
const checkpointPs1Path = join(vibeXBase, 'checkpoint.ps1');
|
|
374
|
+
const allocPatchShPath = join(vibeXBase, 'alloc_patch.sh');
|
|
375
|
+
const allocPatchPs1Path = join(vibeXBase, 'alloc_patch.ps1');
|
|
376
|
+
const checkUncommittedShPath = join(vibeXBase, 'check_uncommitted.sh');
|
|
377
|
+
const checkUncommittedPs1Path = join(vibeXBase, 'check_uncommitted.ps1');
|
|
378
|
+
for (const filePath of [
|
|
379
|
+
checkpointShPath,
|
|
380
|
+
checkpointPs1Path,
|
|
381
|
+
allocPatchShPath,
|
|
382
|
+
allocPatchPs1Path,
|
|
383
|
+
checkUncommittedShPath,
|
|
384
|
+
checkUncommittedPs1Path,
|
|
385
|
+
join(vibeXBase, 'config.yml'),
|
|
386
|
+
]) {
|
|
387
|
+
if (existsSync(filePath)) rmSync(filePath, { force: true });
|
|
285
388
|
}
|
|
286
389
|
|
|
287
390
|
if (existsSync(skillDir)) {
|
|
@@ -326,6 +429,9 @@ function uninstallProjectOnly(targetDir, aiPlatform) {
|
|
|
326
429
|
rmSync(hooksDir, { recursive: true, force: true });
|
|
327
430
|
}
|
|
328
431
|
}
|
|
432
|
+
|
|
433
|
+
// Remove AGENTS.md block
|
|
434
|
+
removeAgentsMdBlock(root);
|
|
329
435
|
}
|
|
330
436
|
|
|
331
437
|
function registerClaudeHook(osType) {
|
|
@@ -423,6 +529,123 @@ function unregisterClaudeHook() {
|
|
|
423
529
|
console.log(` Cleaned config: ${settingsPath}`);
|
|
424
530
|
}
|
|
425
531
|
|
|
532
|
+
// ============================================================
|
|
533
|
+
// Activate logic (AGENTS.md only, with installation check)
|
|
534
|
+
// ============================================================
|
|
535
|
+
|
|
536
|
+
const SUPPORTED_PLATFORMS = ['cursor', 'claude'];
|
|
537
|
+
|
|
538
|
+
function checkGlobalInstallation() {
|
|
539
|
+
const home = homedir();
|
|
540
|
+
const results = {
|
|
541
|
+
hasScripts: existsSync(join(INSTALL_BASE, 'scripts', 'checkpoint.sh')) ||
|
|
542
|
+
existsSync(join(INSTALL_BASE, 'scripts', 'checkpoint.ps1')),
|
|
543
|
+
platforms: {}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
for (const p of SUPPORTED_PLATFORMS) {
|
|
547
|
+
const skillDir = p === 'cursor' ? '.cursor' : '.claude';
|
|
548
|
+
results.platforms[p] = {
|
|
549
|
+
hasSkill: existsSync(join(home, skillDir, 'skills', SKILL_NAME, 'SKILL.md'))
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
results.hasAnySkill = SUPPORTED_PLATFORMS.some(p => results.platforms[p].hasSkill);
|
|
554
|
+
results.isFullyInstalled = results.hasScripts && results.hasAnySkill;
|
|
555
|
+
return results;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function checkProjectInstallation(targetDir) {
|
|
559
|
+
const root = resolve(targetDir);
|
|
560
|
+
const results = {
|
|
561
|
+
hasScripts: existsSync(join(root, '.vibe-x', 'agent-better-checkpoint', 'checkpoint.sh')) ||
|
|
562
|
+
existsSync(join(root, '.vibe-x', 'agent-better-checkpoint', 'checkpoint.ps1')),
|
|
563
|
+
platforms: {}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
for (const p of SUPPORTED_PLATFORMS) {
|
|
567
|
+
const skillDir = p === 'cursor' ? '.cursor' : '.claude';
|
|
568
|
+
results.platforms[p] = {
|
|
569
|
+
hasSkill: existsSync(join(root, skillDir, 'skills', SKILL_NAME, 'SKILL.md'))
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
results.hasAnySkill = SUPPORTED_PLATFORMS.some(p => results.platforms[p].hasSkill);
|
|
574
|
+
results.isFullyInstalled = results.hasScripts && results.hasAnySkill;
|
|
575
|
+
return results;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function activateProject(targetDir) {
|
|
579
|
+
const root = resolve(targetDir);
|
|
580
|
+
|
|
581
|
+
// Check if already has AGENTS.md block
|
|
582
|
+
const agentsMdPath = join(root, 'AGENTS.md');
|
|
583
|
+
if (existsSync(agentsMdPath)) {
|
|
584
|
+
const content = readFileSync(agentsMdPath, 'utf-8');
|
|
585
|
+
if (content.includes(AGENTS_BLOCK_START)) {
|
|
586
|
+
console.log(`\n⚠️ AGENTS.md already contains checkpoint rules. Nothing to do.`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Step 1: Check project-level installation (all platforms)
|
|
592
|
+
const projectStatus = checkProjectInstallation(root);
|
|
593
|
+
if (projectStatus.isFullyInstalled) {
|
|
594
|
+
// Project has both scripts and skill for at least one platform
|
|
595
|
+
console.log(`\n[Activate] Adding checkpoint rules to ${root}...`);
|
|
596
|
+
injectAgentsMdBlock(root);
|
|
597
|
+
console.log(`\n✅ Activation complete!`);
|
|
598
|
+
console.log(`\nThe AI agent will now follow checkpoint commit rules in this project.`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Step 2: Check global installation (all platforms)
|
|
603
|
+
const globalStatus = checkGlobalInstallation();
|
|
604
|
+
if (globalStatus.isFullyInstalled) {
|
|
605
|
+
// Global has both scripts and skill for at least one platform
|
|
606
|
+
console.log(`\n[Activate] Adding checkpoint rules to ${root}...`);
|
|
607
|
+
injectAgentsMdBlock(root);
|
|
608
|
+
console.log(`\n✅ Activation complete!`);
|
|
609
|
+
console.log(`\nThe AI agent will now follow checkpoint commit rules in this project.`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Step 3: Neither project nor global is fully installed - provide detailed diagnosis
|
|
614
|
+
const hasAnyProjectSkill = projectStatus.hasAnySkill;
|
|
615
|
+
const hasAnyGlobalSkill = globalStatus.hasAnySkill;
|
|
616
|
+
const hasAnyProjectScripts = projectStatus.hasScripts;
|
|
617
|
+
const hasAnyGlobalScripts = globalStatus.hasScripts;
|
|
618
|
+
|
|
619
|
+
const hasAnySkill = hasAnyProjectSkill || hasAnyGlobalSkill;
|
|
620
|
+
const hasAnyScripts = hasAnyProjectScripts || hasAnyGlobalScripts;
|
|
621
|
+
|
|
622
|
+
if (!hasAnySkill && !hasAnyScripts) {
|
|
623
|
+
console.log(`\n⚠️ No agent-better-checkpoint installation detected.`);
|
|
624
|
+
console.log(`\nChecked locations:`);
|
|
625
|
+
console.log(` Project: ${root}`);
|
|
626
|
+
console.log(` Global: ${INSTALL_BASE}`);
|
|
627
|
+
console.log(`\nPlease install first:`);
|
|
628
|
+
console.log(` Global: npx @vibe-x/agent-better-checkpoint`);
|
|
629
|
+
console.log(` Project-only: npx @vibe-x/agent-better-checkpoint --target . --platform cursor|claude`);
|
|
630
|
+
console.log(`\nThen run --activate again.`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!hasAnySkill) {
|
|
635
|
+
console.log(`\n⚠️ Checkpoint scripts found, but skill (SKILL.md) is missing.`);
|
|
636
|
+
console.log(`\nThe AI agent needs the skill to know how to commit. Please run:`);
|
|
637
|
+
console.log(` npx @vibe-x/agent-better-checkpoint --platform cursor|claude`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!hasAnyScripts) {
|
|
642
|
+
console.log(`\n⚠️ Skill found, but checkpoint scripts are missing.`);
|
|
643
|
+
console.log(`\nPlease reinstall to get the scripts:`);
|
|
644
|
+
console.log(` npx @vibe-x/agent-better-checkpoint`);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
426
649
|
// ============================================================
|
|
427
650
|
// Main entry
|
|
428
651
|
// ============================================================
|
|
@@ -433,7 +656,7 @@ function main() {
|
|
|
433
656
|
const aiPlatform = args.platform || detectAIPlatform();
|
|
434
657
|
const projectTargetDir = args.target ? resolve(args.target) : null;
|
|
435
658
|
|
|
436
|
-
if (!aiPlatform && !projectTargetDir && !args.uninstall) {
|
|
659
|
+
if (!aiPlatform && !projectTargetDir && !args.uninstall && !args.activate) {
|
|
437
660
|
console.error(
|
|
438
661
|
'Error: could not detect AI platform.\n' +
|
|
439
662
|
'Please specify: npx @vibe-x/agent-better-checkpoint --platform cursor|claude'
|
|
@@ -476,6 +699,10 @@ function main() {
|
|
|
476
699
|
if (platforms.length === 0) console.log('\nNo global installation found.');
|
|
477
700
|
}
|
|
478
701
|
console.log('\n✅ Uninstallation complete!');
|
|
702
|
+
} else if (args.activate) {
|
|
703
|
+
// Activate: only inject AGENTS.md, check installation first
|
|
704
|
+
const targetDir = projectTargetDir || process.cwd();
|
|
705
|
+
activateProject(targetDir);
|
|
479
706
|
} else {
|
|
480
707
|
if (projectTargetDir) {
|
|
481
708
|
if (!aiPlatform) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-x/agent-better-checkpoint",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Semantic Git checkpoint commits for AI coding sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,8 +10,14 @@
|
|
|
10
10
|
"bin/",
|
|
11
11
|
"platform/",
|
|
12
12
|
"skill/",
|
|
13
|
-
"LICENSE"
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"scripts/"
|
|
14
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"release:check": "node ./scripts/release-check.mjs",
|
|
18
|
+
"preversion": "npm run release:check",
|
|
19
|
+
"prepublishOnly": "npm run release:check"
|
|
20
|
+
},
|
|
15
21
|
"keywords": [
|
|
16
22
|
"ai",
|
|
17
23
|
"agent",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# alloc_patch.sh — Allocate a temporary patch file for hunk-level checkpoint commits
|
|
4
|
+
#
|
|
5
|
+
# Creates a writable patch path under ~/.vibe-x/agent-better-checkpoint/tmp/
|
|
6
|
+
# and returns a one-line JSON payload with id, createdAt, path, and ttlHours.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# alloc_patch.sh --workspace /path/to/repo
|
|
10
|
+
# alloc_patch.sh --help
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
TTL_HOURS=48
|
|
15
|
+
TMP_ROOT="${HOME}/.vibe-x/agent-better-checkpoint/tmp"
|
|
16
|
+
WORKSPACE=""
|
|
17
|
+
|
|
18
|
+
usage() {
|
|
19
|
+
cat <<'EOF'
|
|
20
|
+
alloc_patch.sh — Allocate a temporary patch file for checkpoint hunks
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
alloc_patch.sh --workspace <path>
|
|
24
|
+
alloc_patch.sh --help
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--workspace <path> Workspace root used to derive a stable temp namespace
|
|
28
|
+
--help Show this help message
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
error() {
|
|
33
|
+
local code="$1"
|
|
34
|
+
shift
|
|
35
|
+
echo "${code}: $*" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
json_escape() {
|
|
40
|
+
local str="$1"
|
|
41
|
+
str="${str//\\/\\\\}"
|
|
42
|
+
str="${str//\"/\\\"}"
|
|
43
|
+
str="${str//$'\n'/\\n}"
|
|
44
|
+
str="${str//$'\r'/\\r}"
|
|
45
|
+
str="${str//$'\t'/\\t}"
|
|
46
|
+
printf '%s' "$str"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
workspace_hash() {
|
|
50
|
+
local input="$1"
|
|
51
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
52
|
+
printf '%s' "$input" | shasum -a 256 | awk '{print substr($1,1,8)}'
|
|
53
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
54
|
+
printf '%s' "$input" | sha256sum | awk '{print substr($1,1,8)}'
|
|
55
|
+
else
|
|
56
|
+
printf '%s' "$input" | cksum | awk '{print $1}'
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
random_id() {
|
|
61
|
+
local id
|
|
62
|
+
id=$(LC_ALL=C tr -dc 'A-Z0-9' < /dev/urandom | head -c 10 || true)
|
|
63
|
+
if [[ -z "$id" ]]; then
|
|
64
|
+
id="$(date -u +%s)"
|
|
65
|
+
fi
|
|
66
|
+
printf '%s' "$id"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
cleanup_expired() {
|
|
70
|
+
local root="$1"
|
|
71
|
+
[[ -d "$root" ]] || return 0
|
|
72
|
+
|
|
73
|
+
find "$root" -type f -name 'patch-*.patch' -mtime +1 -exec rm -f {} + 2>/dev/null || true
|
|
74
|
+
find "$root" -type d -empty -mindepth 1 -exec rmdir {} + 2>/dev/null || true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
while [[ $# -gt 0 ]]; do
|
|
78
|
+
case "$1" in
|
|
79
|
+
--workspace)
|
|
80
|
+
WORKSPACE="${2:-}"
|
|
81
|
+
shift 2
|
|
82
|
+
;;
|
|
83
|
+
--help|-h)
|
|
84
|
+
usage
|
|
85
|
+
exit 0
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
error "ABC_TEMP_ALLOC_FAILED" "Unknown option: $1"
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
done
|
|
92
|
+
|
|
93
|
+
if [[ -z "$WORKSPACE" ]]; then
|
|
94
|
+
error "ABC_TEMP_ALLOC_FAILED" "Missing required --workspace argument. Please provide the workspace root path."
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
if ! mkdir -p "$TMP_ROOT" 2>/dev/null; then
|
|
98
|
+
error "ABC_TEMP_ALLOC_FAILED" "Failed to create the temp root at ${TMP_ROOT}. Please check directory permissions."
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
cleanup_expired "$TMP_ROOT"
|
|
102
|
+
|
|
103
|
+
WORKSPACE_HASH=$(workspace_hash "$WORKSPACE")
|
|
104
|
+
WORKSPACE_DIR="${TMP_ROOT}/${WORKSPACE_HASH}"
|
|
105
|
+
if ! mkdir -p "$WORKSPACE_DIR" 2>/dev/null; then
|
|
106
|
+
error "ABC_TEMP_ALLOC_FAILED" "Failed to create the workspace temp directory at ${WORKSPACE_DIR}. Please check directory permissions."
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
|
|
110
|
+
CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
111
|
+
ID=$(random_id)
|
|
112
|
+
PATCH_PATH="${WORKSPACE_DIR}/patch-${STAMP}-${ID}.patch"
|
|
113
|
+
|
|
114
|
+
if ! : > "$PATCH_PATH" 2>/dev/null; then
|
|
115
|
+
error "ABC_TEMP_ALLOC_FAILED" "Failed to create a temporary patch file at ${PATCH_PATH}. Please check directory permissions and available disk space."
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
printf '{"ok":true,"id":"%s","createdAt":"%s","path":"%s","ttlHours":%s}\n' \
|
|
119
|
+
"$(json_escape "$ID")" \
|
|
120
|
+
"$(json_escape "$CREATED_AT")" \
|
|
121
|
+
"$(json_escape "$PATCH_PATH")" \
|
|
122
|
+
"$TTL_HOURS"
|
|
@@ -427,7 +427,7 @@ build_and_output_reminder() {
|
|
|
427
427
|
changes_indented=$(echo "$changes" | sed 's/^/ /')
|
|
428
428
|
|
|
429
429
|
# Project-local script; fallback to global
|
|
430
|
-
local checkpoint_cmd_sh checkpoint_cmd_ps1
|
|
430
|
+
local checkpoint_cmd_sh checkpoint_cmd_ps1 alloc_patch_cmd_sh alloc_patch_cmd_ps1
|
|
431
431
|
if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/checkpoint.sh" ]]; then
|
|
432
432
|
checkpoint_cmd_sh=".vibe-x/agent-better-checkpoint/checkpoint.sh"
|
|
433
433
|
else
|
|
@@ -438,6 +438,16 @@ build_and_output_reminder() {
|
|
|
438
438
|
else
|
|
439
439
|
checkpoint_cmd_ps1='\$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/checkpoint.ps1'
|
|
440
440
|
fi
|
|
441
|
+
if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/alloc_patch.sh" ]]; then
|
|
442
|
+
alloc_patch_cmd_sh=".vibe-x/agent-better-checkpoint/alloc_patch.sh"
|
|
443
|
+
else
|
|
444
|
+
alloc_patch_cmd_sh="~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh"
|
|
445
|
+
fi
|
|
446
|
+
if [[ -f "${workspace}/.vibe-x/agent-better-checkpoint/alloc_patch.ps1" ]]; then
|
|
447
|
+
alloc_patch_cmd_ps1='.\\.vibe-x\\agent-better-checkpoint\\alloc_patch.ps1'
|
|
448
|
+
else
|
|
449
|
+
alloc_patch_cmd_ps1='\$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.ps1'
|
|
450
|
+
fi
|
|
441
451
|
|
|
442
452
|
local reminder
|
|
443
453
|
reminder="## ⚠️ Uncommitted Changes Detected
|
|
@@ -449,17 +459,24 @@ There are uncommitted changes in the workspace. Please create a checkpoint commi
|
|
|
449
459
|
${changes_indented}
|
|
450
460
|
\`\`\`
|
|
451
461
|
|
|
452
|
-
**Action Required**:
|
|
462
|
+
**Action Required**: Allocate a patch file, write only the selected hunks for this conversation, then run the checkpoint script.
|
|
453
463
|
|
|
454
464
|
**macOS/Linux:**
|
|
455
465
|
\`\`\`bash
|
|
456
|
-
${
|
|
466
|
+
PATCH_JSON=\$(${alloc_patch_cmd_sh} --workspace \"${workspace}\")
|
|
467
|
+
PATCH_PATH=\$(printf '%s' \"\$PATCH_JSON\" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
|
|
468
|
+
# Write only the selected hunks for this conversation to \"\$PATCH_PATH\" as a unified diff patch.
|
|
469
|
+
${checkpoint_cmd_sh} \"checkpoint(<scope>): <description>\" \"<user-prompt>\" --type fallback --patch-file \"\$PATCH_PATH\"
|
|
457
470
|
\`\`\`
|
|
458
471
|
|
|
459
472
|
**Windows (PowerShell):**
|
|
460
473
|
\`\`\`powershell
|
|
461
|
-
powershell -File \"${
|
|
462
|
-
|
|
474
|
+
\$patch = powershell -File \"${alloc_patch_cmd_ps1}\" -Workspace \"${workspace}\" | ConvertFrom-Json
|
|
475
|
+
# Write only the selected hunks for this conversation to \$patch.path as a unified diff patch.
|
|
476
|
+
powershell -File \"${checkpoint_cmd_ps1}\" \"checkpoint(<scope>): <description>\" \"<user-prompt>\" -Type fallback -PatchFile \$patch.path
|
|
477
|
+
\`\`\`
|
|
478
|
+
|
|
479
|
+
If the checkpoint script returns an \`ABC_PATCH_*\` error, do not fall back to a full commit automatically. Explain the conflict and ask the user how to proceed."
|
|
463
480
|
|
|
464
481
|
output_block "$reminder" "$platform"
|
|
465
482
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
# AI provides descriptive content (subject + body). This script:
|
|
6
6
|
# 1. Truncates user-prompt (≤60 chars, head+tail)
|
|
7
7
|
# 2. Appends metadata via git interpret-trailers
|
|
8
|
-
# 3. Runs git add -A && git commit
|
|
8
|
+
# 3. Runs git add -A && git commit, or applies a selected patch to the index first
|
|
9
9
|
#
|
|
10
10
|
# Usage:
|
|
11
|
-
# checkpoint.sh <message> [user-prompt] [--type auto|fallback]
|
|
11
|
+
# checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]
|
|
12
12
|
|
|
13
13
|
set -euo pipefail
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ set -euo pipefail
|
|
|
18
18
|
MESSAGE="${1:-}"
|
|
19
19
|
USER_PROMPT="${2:-}"
|
|
20
20
|
CHECKPOINT_TYPE="auto"
|
|
21
|
+
PATCH_FILE=""
|
|
21
22
|
|
|
22
23
|
shift 2 2>/dev/null || true
|
|
23
24
|
while [[ $# -gt 0 ]]; do
|
|
@@ -26,26 +27,53 @@ while [[ $# -gt 0 ]]; do
|
|
|
26
27
|
CHECKPOINT_TYPE="${2:-auto}"
|
|
27
28
|
shift 2
|
|
28
29
|
;;
|
|
30
|
+
--patch-file)
|
|
31
|
+
PATCH_FILE="${2:-}"
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
34
|
+
--help|-h)
|
|
35
|
+
cat <<'EOF'
|
|
36
|
+
checkpoint.sh — Create semantic Git checkpoint commits
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--type <auto|fallback> Checkpoint type metadata
|
|
43
|
+
--patch-file <path> Apply only the selected patch hunks to the index before commit
|
|
44
|
+
--help Show this help message
|
|
45
|
+
EOF
|
|
46
|
+
exit 0
|
|
47
|
+
;;
|
|
29
48
|
*)
|
|
30
|
-
|
|
49
|
+
echo "Unknown option: $1" >&2
|
|
50
|
+
exit 1
|
|
31
51
|
;;
|
|
32
52
|
esac
|
|
33
53
|
done
|
|
34
54
|
|
|
35
55
|
if [[ -z "$MESSAGE" ]]; then
|
|
36
56
|
echo "Error: commit message is required" >&2
|
|
37
|
-
echo "Usage: checkpoint.sh <message> [user-prompt] [--type auto|fallback]" >&2
|
|
57
|
+
echo "Usage: checkpoint.sh <message> [user-prompt] [--type auto|fallback] [--patch-file <path>]" >&2
|
|
38
58
|
exit 1
|
|
39
59
|
fi
|
|
40
60
|
|
|
61
|
+
fail_checkpoint() {
|
|
62
|
+
local code="$1"
|
|
63
|
+
shift
|
|
64
|
+
echo "${code}: $*" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
# ============================================================
|
|
42
69
|
# Platform detection
|
|
43
70
|
# ============================================================
|
|
44
71
|
detect_platform() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
elif [[ -n "${CURSOR_VERSION:-}" ]] || [[ -n "${CURSOR_TRACE_ID:-}" ]]; then
|
|
72
|
+
# 优先检测运行时环境变量(谁在调用),而非安装态(command -v)
|
|
73
|
+
if [[ -n "${CURSOR_AGENT:-}" ]] || [[ -n "${CURSOR_TRACE_ID:-}" ]] || [[ -n "${CURSOR_VERSION:-}" ]]; then
|
|
48
74
|
echo "cursor"
|
|
75
|
+
elif [[ -n "${CLAUDE_CODE:-}" ]]; then
|
|
76
|
+
echo "claude-code"
|
|
49
77
|
else
|
|
50
78
|
echo "unknown"
|
|
51
79
|
fi
|
|
@@ -59,10 +87,23 @@ AGENT_PLATFORM=$(detect_platform)
|
|
|
59
87
|
truncate_prompt() {
|
|
60
88
|
local prompt="$1"
|
|
61
89
|
local max_len=60
|
|
90
|
+
|
|
91
|
+
# Ensure UTF-8 character-based (not byte-based) string operations.
|
|
92
|
+
# Without this, ${#prompt} and ${prompt:0:n} count bytes in C locale,
|
|
93
|
+
# which breaks multi-byte characters (e.g., Chinese) mid-character.
|
|
94
|
+
local old_lc_all="${LC_ALL:-}"
|
|
95
|
+
export LC_ALL=en_US.UTF-8
|
|
96
|
+
|
|
62
97
|
local len=${#prompt}
|
|
63
98
|
|
|
64
99
|
if [[ $len -le $max_len ]]; then
|
|
65
100
|
echo "$prompt"
|
|
101
|
+
# Restore LC_ALL
|
|
102
|
+
if [[ -n "$old_lc_all" ]]; then
|
|
103
|
+
export LC_ALL="$old_lc_all"
|
|
104
|
+
else
|
|
105
|
+
unset LC_ALL
|
|
106
|
+
fi
|
|
66
107
|
return
|
|
67
108
|
fi
|
|
68
109
|
|
|
@@ -70,6 +111,14 @@ truncate_prompt() {
|
|
|
70
111
|
local tail_len=$(( max_len - 3 - head_len ))
|
|
71
112
|
local head="${prompt:0:$head_len}"
|
|
72
113
|
local tail="${prompt:$((len - tail_len)):$tail_len}"
|
|
114
|
+
|
|
115
|
+
# Restore LC_ALL
|
|
116
|
+
if [[ -n "$old_lc_all" ]]; then
|
|
117
|
+
export LC_ALL="$old_lc_all"
|
|
118
|
+
else
|
|
119
|
+
unset LC_ALL
|
|
120
|
+
fi
|
|
121
|
+
|
|
73
122
|
echo "${head}...${tail}"
|
|
74
123
|
}
|
|
75
124
|
|
|
@@ -97,15 +146,44 @@ has_changes() {
|
|
|
97
146
|
return 1
|
|
98
147
|
}
|
|
99
148
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
fi
|
|
149
|
+
has_staged_changes() {
|
|
150
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
104
155
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
if [[ -z "$PATCH_FILE" ]]; then
|
|
157
|
+
if ! has_changes; then
|
|
158
|
+
echo "No changes to commit."
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# ============================================================
|
|
163
|
+
# git add -A
|
|
164
|
+
# ============================================================
|
|
165
|
+
git add -A
|
|
166
|
+
else
|
|
167
|
+
if [[ ! -f "$PATCH_FILE" ]]; then
|
|
168
|
+
fail_checkpoint "ABC_PATCH_FILE_MISSING" "Patch file not found at ${PATCH_FILE}. Please allocate and write the patch file before running checkpoint."
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
if [[ ! -s "$PATCH_FILE" ]]; then
|
|
172
|
+
fail_checkpoint "ABC_PATCH_FILE_EMPTY" "Patch file at ${PATCH_FILE} is empty. Please write at least one selected hunk before running checkpoint."
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
if ! git apply --cached --check "$PATCH_FILE" >/dev/null 2>&1; then
|
|
176
|
+
fail_checkpoint "ABC_PATCH_APPLY_FAILED" "Failed to apply the selected patch cleanly to the Git index. The target hunks may have drifted or overlap with other uncommitted edits."
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if ! git apply --cached "$PATCH_FILE" >/dev/null 2>&1; then
|
|
180
|
+
fail_checkpoint "ABC_PATCH_APPLY_FAILED" "Failed to apply the selected patch to the Git index after the dry run succeeded. Please verify the patch file and retry."
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if ! has_staged_changes; then
|
|
184
|
+
fail_checkpoint "ABC_PATCH_NO_STAGED_CHANGES" "The selected patch did not produce any staged changes. The target hunks may already be staged or no longer match the current repository state."
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
109
187
|
|
|
110
188
|
# ============================================================
|
|
111
189
|
# Build trailers and commit
|
|
@@ -119,6 +197,15 @@ if [[ -n "$TRUNCATED_PROMPT" ]]; then
|
|
|
119
197
|
TRAILER_ARGS+=(--trailer "User-Prompt: ${TRUNCATED_PROMPT}")
|
|
120
198
|
fi
|
|
121
199
|
|
|
122
|
-
echo "$MESSAGE" | git interpret-trailers "${TRAILER_ARGS[@]}" | git commit -F
|
|
200
|
+
if ! echo "$MESSAGE" | git interpret-trailers "${TRAILER_ARGS[@]}" | git commit -F -; then
|
|
201
|
+
if [[ -n "$PATCH_FILE" ]]; then
|
|
202
|
+
fail_checkpoint "ABC_PATCH_COMMIT_FAILED" "Git commit failed after staging the selected patch. Review the repository state and retry when ready."
|
|
203
|
+
fi
|
|
204
|
+
exit 1
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [[ -n "$PATCH_FILE" ]]; then
|
|
208
|
+
rm -f "$PATCH_FILE"
|
|
209
|
+
fi
|
|
123
210
|
|
|
124
211
|
echo "Checkpoint committed successfully."
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
alloc_patch.ps1 — Allocate a temporary patch file for hunk-level checkpoint commits
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Creates a writable patch path under ~/.vibe-x/agent-better-checkpoint/tmp/
|
|
7
|
+
and returns a one-line JSON payload with id, createdAt, path, and ttlHours.
|
|
8
|
+
|
|
9
|
+
.PARAMETER Workspace
|
|
10
|
+
Workspace root used to derive a stable temp namespace. Required.
|
|
11
|
+
|
|
12
|
+
.EXAMPLE
|
|
13
|
+
.\alloc_patch.ps1 -Workspace C:\repo
|
|
14
|
+
#>
|
|
15
|
+
|
|
16
|
+
param(
|
|
17
|
+
[string]$Workspace = "",
|
|
18
|
+
[switch]$Help
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
$ErrorActionPreference = "Stop"
|
|
22
|
+
$TTL_HOURS = 48
|
|
23
|
+
$TMP_ROOT = Join-Path $HOME ".vibe-x/agent-better-checkpoint/tmp"
|
|
24
|
+
|
|
25
|
+
function Show-Usage {
|
|
26
|
+
@"
|
|
27
|
+
alloc_patch.ps1 — Allocate a temporary patch file for checkpoint hunks
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
alloc_patch.ps1 -Workspace <path>
|
|
31
|
+
alloc_patch.ps1 -Help
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-Workspace <path> Workspace root used to derive a stable temp namespace
|
|
35
|
+
-Help Show this help message
|
|
36
|
+
"@ | Write-Output
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Fail-Alloc {
|
|
40
|
+
param(
|
|
41
|
+
[string]$Code,
|
|
42
|
+
[string]$Message
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
[Console]::Error.WriteLine("${Code}: ${Message}")
|
|
46
|
+
exit 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Get-WorkspaceHash {
|
|
50
|
+
param([string]$InputText)
|
|
51
|
+
|
|
52
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
53
|
+
try {
|
|
54
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($InputText)
|
|
55
|
+
$hashBytes = $sha.ComputeHash($bytes)
|
|
56
|
+
$hex = -join ($hashBytes | ForEach-Object { $_.ToString("x2") })
|
|
57
|
+
return $hex.Substring(0, 8)
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
$sha.Dispose()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Get-RandomId {
|
|
65
|
+
$bytes = New-Object byte[] 6
|
|
66
|
+
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
|
|
67
|
+
$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })
|
|
68
|
+
return $hex.Substring(0, 10)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Cleanup-Expired {
|
|
72
|
+
param([string]$Root)
|
|
73
|
+
|
|
74
|
+
if (-not (Test-Path $Root -PathType Container)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$cutoff = (Get-Date).ToUniversalTime().AddHours(-$TTL_HOURS)
|
|
79
|
+
Get-ChildItem -Path $Root -Recurse -File -Filter 'patch-*.patch' -ErrorAction SilentlyContinue |
|
|
80
|
+
Where-Object { $_.LastWriteTimeUtc -lt $cutoff } |
|
|
81
|
+
ForEach-Object {
|
|
82
|
+
Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Get-ChildItem -Path $Root -Recurse -Directory -ErrorAction SilentlyContinue |
|
|
86
|
+
Sort-Object FullName -Descending |
|
|
87
|
+
ForEach-Object {
|
|
88
|
+
if (-not (Get-ChildItem -Path $_.FullName -Force -ErrorAction SilentlyContinue)) {
|
|
89
|
+
Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ($Help) {
|
|
95
|
+
Show-Usage
|
|
96
|
+
exit 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (-not $Workspace) {
|
|
100
|
+
Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Missing required -Workspace argument. Please provide the workspace root path."
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
New-Item -ItemType Directory -Path $TMP_ROOT -Force | Out-Null
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create the temp root at $TMP_ROOT. Please check directory permissions."
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Cleanup-Expired -Root $TMP_ROOT
|
|
111
|
+
|
|
112
|
+
$workspaceHash = Get-WorkspaceHash -InputText $Workspace
|
|
113
|
+
$workspaceDir = Join-Path $TMP_ROOT $workspaceHash
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
New-Item -ItemType Directory -Path $workspaceDir -Force | Out-Null
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create the workspace temp directory at $workspaceDir. Please check directory permissions."
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$stamp = (Get-Date).ToUniversalTime().ToString("yyyyMMddTHHmmssZ")
|
|
123
|
+
$createdAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
124
|
+
$id = Get-RandomId
|
|
125
|
+
$patchPath = Join-Path $workspaceDir "patch-$stamp-$id.patch"
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
New-Item -ItemType File -Path $patchPath -Force | Out-Null
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
Fail-Alloc -Code "ABC_TEMP_ALLOC_FAILED" -Message "Failed to create a temporary patch file at $patchPath. Please check directory permissions and available disk space."
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
$result = [ordered]@{
|
|
135
|
+
ok = $true
|
|
136
|
+
id = $id
|
|
137
|
+
createdAt = $createdAt
|
|
138
|
+
path = $patchPath
|
|
139
|
+
ttlHours = $TTL_HOURS
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
$result | ConvertTo-Json -Compress | Write-Output
|
|
@@ -288,12 +288,20 @@ function Build-Reminder {
|
|
|
288
288
|
# Project-local script; fallback to global
|
|
289
289
|
$checkpointSh = "~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh"
|
|
290
290
|
$checkpointPs1 = "`$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/checkpoint.ps1"
|
|
291
|
+
$allocPatchSh = "~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh"
|
|
292
|
+
$allocPatchPs1 = "`$env:USERPROFILE/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.ps1"
|
|
291
293
|
if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/checkpoint.sh")) {
|
|
292
294
|
$checkpointSh = ".vibe-x/agent-better-checkpoint/checkpoint.sh"
|
|
293
295
|
}
|
|
294
296
|
if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/checkpoint.ps1")) {
|
|
295
297
|
$checkpointPs1 = ".\.vibe-x\agent-better-checkpoint\checkpoint.ps1"
|
|
296
298
|
}
|
|
299
|
+
if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/alloc_patch.sh")) {
|
|
300
|
+
$allocPatchSh = ".vibe-x/agent-better-checkpoint/alloc_patch.sh"
|
|
301
|
+
}
|
|
302
|
+
if (Test-Path (Join-Path $Workspace ".vibe-x/agent-better-checkpoint/alloc_patch.ps1")) {
|
|
303
|
+
$allocPatchPs1 = ".\.vibe-x\agent-better-checkpoint\alloc_patch.ps1"
|
|
304
|
+
}
|
|
297
305
|
|
|
298
306
|
return @"
|
|
299
307
|
## ⚠️ Uncommitted Changes Detected
|
|
@@ -305,17 +313,24 @@ There are uncommitted changes in the workspace. Please create a checkpoint commi
|
|
|
305
313
|
$ChangesIndented
|
|
306
314
|
``````
|
|
307
315
|
|
|
308
|
-
**Action Required**:
|
|
316
|
+
**Action Required**: Allocate a patch file, write only the selected hunks for this conversation, then run the checkpoint script.
|
|
309
317
|
|
|
310
318
|
**macOS/Linux:**
|
|
311
319
|
``````bash
|
|
312
|
-
$
|
|
320
|
+
PATCH_JSON=$($allocPatchSh --workspace "$Workspace")
|
|
321
|
+
PATCH_PATH=$(printf '%s' "$PATCH_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
|
|
322
|
+
# Write only the selected hunks for this conversation to "$PATCH_PATH" as a unified diff patch.
|
|
323
|
+
$checkpointSh "checkpoint(<scope>): <description>" "<user-prompt>" --type fallback --patch-file "$PATCH_PATH"
|
|
313
324
|
``````
|
|
314
325
|
|
|
315
326
|
**Windows (PowerShell):**
|
|
316
327
|
``````powershell
|
|
317
|
-
powershell -File "$
|
|
328
|
+
$patch = powershell -File "$allocPatchPs1" -Workspace "$Workspace" | ConvertFrom-Json
|
|
329
|
+
# Write only the selected hunks for this conversation to $patch.path as a unified diff patch.
|
|
330
|
+
powershell -File "$checkpointPs1" "checkpoint(<scope>): <description>" "<user-prompt>" -Type fallback -PatchFile $patch.path
|
|
318
331
|
``````
|
|
332
|
+
|
|
333
|
+
If the checkpoint script returns an `ABC_PATCH_*` error, do not fall back to a full commit automatically. Explain the conflict and ask the user how to proceed.
|
|
319
334
|
"@
|
|
320
335
|
}
|
|
321
336
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
AI provides descriptive content (subject + body). This script:
|
|
7
7
|
1. Truncates user-prompt (≤60 chars, head+tail)
|
|
8
8
|
2. Appends metadata via git interpret-trailers
|
|
9
|
-
3. Runs git add -A && git commit
|
|
9
|
+
3. Runs git add -A && git commit, or applies a selected patch to the index first
|
|
10
10
|
|
|
11
11
|
.PARAMETER Message
|
|
12
12
|
Full commit message (subject + blank line + body). Required.
|
|
@@ -17,33 +17,77 @@
|
|
|
17
17
|
.PARAMETER Type
|
|
18
18
|
Checkpoint type: "auto" (default) or "fallback".
|
|
19
19
|
|
|
20
|
+
.PARAMETER PatchFile
|
|
21
|
+
Optional patch file path. When provided, only the selected hunks are staged before commit.
|
|
22
|
+
|
|
20
23
|
.EXAMPLE
|
|
21
24
|
.\checkpoint.ps1 "checkpoint(auth): add JWT refresh" "implement token refresh"
|
|
22
25
|
#>
|
|
23
26
|
|
|
24
27
|
param(
|
|
25
|
-
[Parameter(
|
|
26
|
-
[string]$Message,
|
|
28
|
+
[Parameter(Position = 0)]
|
|
29
|
+
[string]$Message = "",
|
|
27
30
|
|
|
28
31
|
[Parameter(Position = 1)]
|
|
29
32
|
[string]$UserPrompt = "",
|
|
30
33
|
|
|
31
|
-
[string]$Type = "auto"
|
|
34
|
+
[string]$Type = "auto",
|
|
35
|
+
|
|
36
|
+
[string]$PatchFile = "",
|
|
37
|
+
|
|
38
|
+
[switch]$Help
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
$ErrorActionPreference = "Stop"
|
|
35
42
|
|
|
43
|
+
function Show-Usage {
|
|
44
|
+
@"
|
|
45
|
+
checkpoint.ps1 — Create semantic Git checkpoint commits
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
checkpoint.ps1 <message> [user-prompt] [-Type auto|fallback] [-PatchFile <path>]
|
|
49
|
+
checkpoint.ps1 -Help
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
-Type <auto|fallback> Checkpoint type metadata
|
|
53
|
+
-PatchFile <path> Apply only the selected patch hunks to the index before commit
|
|
54
|
+
-Help Show this help message
|
|
55
|
+
"@ | Write-Output
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Fail-Checkpoint {
|
|
59
|
+
param(
|
|
60
|
+
[string]$Code,
|
|
61
|
+
[string]$MessageText
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
[Console]::Error.WriteLine("${Code}: ${MessageText}")
|
|
65
|
+
exit 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ($Help) {
|
|
69
|
+
Show-Usage
|
|
70
|
+
exit 0
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (-not $Message) {
|
|
74
|
+
[Console]::Error.WriteLine("Error: commit message is required")
|
|
75
|
+
[Console]::Error.WriteLine("Usage: checkpoint.ps1 <message> [user-prompt] [-Type auto|fallback] [-PatchFile <path>]")
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
|
|
36
79
|
# ============================================================
|
|
37
80
|
# Platform detection
|
|
38
81
|
# ============================================================
|
|
39
82
|
|
|
40
83
|
function Detect-Platform {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
if ($env:CURSOR_VERSION -or $env:CURSOR_TRACE_ID) {
|
|
84
|
+
# 优先检测运行时环境变量(谁在调用),而非安装态(Get-Command)
|
|
85
|
+
if ($env:CURSOR_AGENT -or $env:CURSOR_TRACE_ID -or $env:CURSOR_VERSION) {
|
|
45
86
|
return "cursor"
|
|
46
87
|
}
|
|
88
|
+
if ($env:CLAUDE_CODE) {
|
|
89
|
+
return "claude-code"
|
|
90
|
+
}
|
|
47
91
|
return "unknown"
|
|
48
92
|
}
|
|
49
93
|
|
|
@@ -79,11 +123,11 @@ if ($UserPrompt) {
|
|
|
79
123
|
|
|
80
124
|
function Test-HasChanges {
|
|
81
125
|
# Staged changes
|
|
82
|
-
|
|
126
|
+
git diff --cached --quiet 2>$null
|
|
83
127
|
if ($LASTEXITCODE -ne 0) { return $true }
|
|
84
128
|
|
|
85
129
|
# Unstaged changes
|
|
86
|
-
|
|
130
|
+
git diff --quiet 2>$null
|
|
87
131
|
if ($LASTEXITCODE -ne 0) { return $true }
|
|
88
132
|
|
|
89
133
|
# Untracked files
|
|
@@ -93,16 +137,46 @@ function Test-HasChanges {
|
|
|
93
137
|
return $false
|
|
94
138
|
}
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
function Test-HasStagedChanges {
|
|
141
|
+
git diff --cached --quiet 2>$null
|
|
142
|
+
return $LASTEXITCODE -ne 0
|
|
99
143
|
}
|
|
100
144
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
if (-not $PatchFile) {
|
|
146
|
+
if (-not (Test-HasChanges)) {
|
|
147
|
+
Write-Host "No changes to commit."
|
|
148
|
+
exit 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# ============================================================
|
|
152
|
+
# git add -A
|
|
153
|
+
# ============================================================
|
|
154
|
+
git add -A
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
if (-not (Test-Path $PatchFile -PathType Leaf)) {
|
|
158
|
+
Fail-Checkpoint -Code "ABC_PATCH_FILE_MISSING" -MessageText "Patch file not found at $PatchFile. Please allocate and write the patch file before running checkpoint."
|
|
159
|
+
}
|
|
104
160
|
|
|
105
|
-
|
|
161
|
+
$patchInfo = Get-Item $PatchFile
|
|
162
|
+
if ($patchInfo.Length -le 0) {
|
|
163
|
+
Fail-Checkpoint -Code "ABC_PATCH_FILE_EMPTY" -MessageText "Patch file at $PatchFile is empty. Please write at least one selected hunk before running checkpoint."
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
git apply --cached --check -- $PatchFile 2>$null
|
|
167
|
+
if ($LASTEXITCODE -ne 0) {
|
|
168
|
+
Fail-Checkpoint -Code "ABC_PATCH_APPLY_FAILED" -MessageText "Failed to apply the selected patch cleanly to the Git index. The target hunks may have drifted or overlap with other uncommitted edits."
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
git apply --cached -- $PatchFile 2>$null
|
|
172
|
+
if ($LASTEXITCODE -ne 0) {
|
|
173
|
+
Fail-Checkpoint -Code "ABC_PATCH_APPLY_FAILED" -MessageText "Failed to apply the selected patch to the Git index after the dry run succeeded. Please verify the patch file and retry."
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (-not (Test-HasStagedChanges)) {
|
|
177
|
+
Fail-Checkpoint -Code "ABC_PATCH_NO_STAGED_CHANGES" -MessageText "The selected patch did not produce any staged changes. The target hunks may already be staged or no longer match the current repository state."
|
|
178
|
+
}
|
|
179
|
+
}
|
|
106
180
|
|
|
107
181
|
# ============================================================
|
|
108
182
|
# Build trailers and commit
|
|
@@ -117,7 +191,16 @@ if ($TruncatedPrompt) {
|
|
|
117
191
|
$TrailerArgs += @("--trailer", "User-Prompt: $TruncatedPrompt")
|
|
118
192
|
}
|
|
119
193
|
|
|
120
|
-
# Pipe message → git interpret-trailers → git commit
|
|
121
194
|
$Message | git interpret-trailers @TrailerArgs | git commit -F -
|
|
195
|
+
if ($LASTEXITCODE -ne 0) {
|
|
196
|
+
if ($PatchFile) {
|
|
197
|
+
Fail-Checkpoint -Code "ABC_PATCH_COMMIT_FAILED" -MessageText "Git commit failed after staging the selected patch. Review the repository state and retry when ready."
|
|
198
|
+
}
|
|
199
|
+
exit $LASTEXITCODE
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if ($PatchFile -and (Test-Path $PatchFile -PathType Leaf)) {
|
|
203
|
+
Remove-Item $PatchFile -Force -ErrorAction SilentlyContinue
|
|
204
|
+
}
|
|
122
205
|
|
|
123
206
|
Write-Host "Checkpoint committed successfully."
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Release Gate for agent-better-checkpoint.
|
|
4
|
+
* Fails fast when common release omissions are detected.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
function fail(msg) {
|
|
12
|
+
console.error(`\n[release:check] ${msg}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ok(msg) {
|
|
17
|
+
console.log(`[release:check] OK: ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fileExists(path) {
|
|
21
|
+
try {
|
|
22
|
+
readFileSync(path);
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha256(text) {
|
|
30
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readText(path) {
|
|
34
|
+
return readFileSync(path, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(args) {
|
|
38
|
+
return execFileSync('git', args, { encoding: 'utf8' }).trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureCleanWorktree() {
|
|
42
|
+
const out = git(['status', '--porcelain']);
|
|
43
|
+
if (out.length !== 0) fail('Working tree is not clean. Commit or stash changes before release.');
|
|
44
|
+
ok('working tree clean');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureChangelogHasVersion(version) {
|
|
48
|
+
const changelog = readText('CHANGELOG.md');
|
|
49
|
+
if (!changelog.includes(`## [${version}]`)) {
|
|
50
|
+
fail(`CHANGELOG.md does not contain an entry for version ${version}.`);
|
|
51
|
+
}
|
|
52
|
+
ok(`CHANGELOG contains ${version}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureFilesInSync(sourcePath, mirrorPath, label) {
|
|
56
|
+
if (!fileExists(mirrorPath)) {
|
|
57
|
+
console.warn(`[release:check] WARN: ${mirrorPath} not found; skipping sync check for ${label}.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sourceText = readText(sourcePath).replace(/\r\n/g, '\n');
|
|
62
|
+
const mirrorText = readText(mirrorPath).replace(/\r\n/g, '\n');
|
|
63
|
+
|
|
64
|
+
if (sha256(sourceText) !== sha256(mirrorText)) {
|
|
65
|
+
fail(`${label} out of sync:\n- ${sourcePath}\n- ${mirrorPath}\nPlease update both or regenerate project-local copy.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ok(`${label} copies are in sync`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureProjectLocalScriptsInSync() {
|
|
72
|
+
const pairs = [
|
|
73
|
+
{
|
|
74
|
+
sourcePath: 'platform/unix/checkpoint.sh',
|
|
75
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/checkpoint.sh',
|
|
76
|
+
label: 'unix checkpoint.sh',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
sourcePath: 'platform/unix/check_uncommitted.sh',
|
|
80
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/check_uncommitted.sh',
|
|
81
|
+
label: 'unix check_uncommitted.sh',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
sourcePath: 'platform/win/checkpoint.ps1',
|
|
85
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/checkpoint.ps1',
|
|
86
|
+
label: 'windows checkpoint.ps1',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
sourcePath: 'platform/win/check_uncommitted.ps1',
|
|
90
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/check_uncommitted.ps1',
|
|
91
|
+
label: 'windows check_uncommitted.ps1',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
sourcePath: 'platform/unix/alloc_patch.sh',
|
|
95
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/alloc_patch.sh',
|
|
96
|
+
label: 'unix alloc_patch.sh',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
sourcePath: 'platform/win/alloc_patch.ps1',
|
|
100
|
+
mirrorPath: '.vibe-x/agent-better-checkpoint/alloc_patch.ps1',
|
|
101
|
+
label: 'windows alloc_patch.ps1',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const pair of pairs) {
|
|
106
|
+
ensureFilesInSync(pair.sourcePath, pair.mirrorPath, pair.label);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureSkillVersionMatches(version) {
|
|
111
|
+
const skill = readText('skill/SKILL.md');
|
|
112
|
+
const requiredSnippets = [
|
|
113
|
+
`version: "${version}"`,
|
|
114
|
+
`@vibe-x/agent-better-checkpoint@${version}`,
|
|
115
|
+
`**Version**: ${version}`,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const missing = requiredSnippets.filter((snippet) => !skill.includes(snippet));
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
fail(`skill/SKILL.md is missing current version references for ${version}:\n- ${missing.join('\n- ')}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ok(`skill/SKILL.md references ${version}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function main() {
|
|
127
|
+
if (!fileExists('package.json')) fail('package.json not found (run from repo root).');
|
|
128
|
+
if (!fileExists('CHANGELOG.md')) fail('CHANGELOG.md not found.');
|
|
129
|
+
if (!fileExists('platform/unix/checkpoint.sh')) fail('platform/unix/checkpoint.sh not found.');
|
|
130
|
+
|
|
131
|
+
const pkg = JSON.parse(readText('package.json'));
|
|
132
|
+
const version = pkg.version;
|
|
133
|
+
if (!version) fail('package.json missing version field.');
|
|
134
|
+
|
|
135
|
+
ensureCleanWorktree();
|
|
136
|
+
ensureProjectLocalScriptsInSync();
|
|
137
|
+
ensureChangelogHasVersion(version);
|
|
138
|
+
ensureSkillVersionMatches(version);
|
|
139
|
+
|
|
140
|
+
ok(`release checks passed for ${pkg.name}@${version}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main();
|
package/skill/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-better-checkpoint
|
|
3
|
-
description: "
|
|
3
|
+
description: "Creates semantic Git checkpoint commits during AI coding sessions with Conventional Commits format and Git Trailers. Use after completing code edits, or when user says 'commit changes', 'create checkpoint', or 'save my progress'."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.3.
|
|
6
|
+
version: "0.3.4"
|
|
7
7
|
author: "alienzhou"
|
|
8
8
|
category: "version-control"
|
|
9
9
|
---
|
|
@@ -31,7 +31,7 @@ Both `.sh` and `.ps1` are always installed regardless of current OS.
|
|
|
31
31
|
If neither exists, run:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npx @vibe-x/agent-better-checkpoint@0.3.
|
|
34
|
+
npx @vibe-x/agent-better-checkpoint@0.3.4
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Without `--target`: installs globally. With `--target .`: project-only (skill + hooks in `.cursor/`, scripts in `.vibe-x/`), no global changes.
|
|
@@ -99,14 +99,16 @@ Switch to flex-column layout with collapsible sidebar.
|
|
|
99
99
|
|
|
100
100
|
## 🛠️ How to Commit
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Default to the patch-based flow when you can clearly identify the hunks from the current logical unit of work. Fall back to the full-workspace commit flow only when the user explicitly wants everything committed together.
|
|
103
103
|
|
|
104
104
|
**Prefer project-local when present**, fall back to global:
|
|
105
105
|
|
|
106
|
-
|
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
|
106
|
+
| Script | macOS/Linux | Windows |
|
|
107
|
+
|--------|-------------|---------|
|
|
108
|
+
| checkpoint | `.vibe-x/agent-better-checkpoint/checkpoint.sh` | `powershell -File ".vibe-x\agent-better-checkpoint\checkpoint.ps1"` |
|
|
109
|
+
| alloc_patch | `.vibe-x/agent-better-checkpoint/alloc_patch.sh` | `powershell -File ".vibe-x\agent-better-checkpoint\alloc_patch.ps1"` |
|
|
110
|
+
| global checkpoint | `~/.vibe-x/agent-better-checkpoint/scripts/checkpoint.sh` | `powershell -File "$env:USERPROFILE\.vibe-x\agent-better-checkpoint\scripts\checkpoint.ps1"` |
|
|
111
|
+
| global alloc_patch | `~/.vibe-x/agent-better-checkpoint/scripts/alloc_patch.sh` | `powershell -File "$env:USERPROFILE\.vibe-x\agent-better-checkpoint\scripts\alloc_patch.ps1"` |
|
|
110
112
|
|
|
111
113
|
### Parameters:
|
|
112
114
|
|
|
@@ -115,31 +117,51 @@ Call the checkpoint script after composing your message. Both `.sh` and `.ps1` a
|
|
|
115
117
|
| message (1st arg) | Yes | Full commit message (subject + blank line + body) |
|
|
116
118
|
| user-prompt (2nd arg) | No | The user's original prompt/request |
|
|
117
119
|
| `--type` / `-Type` | No | `auto` (default) or `fallback` |
|
|
120
|
+
| `--patch-file` / `-PatchFile` | No | Apply only the selected hunks from a unified diff patch before commit |
|
|
118
121
|
|
|
119
|
-
###
|
|
122
|
+
### Recommended patch flow (macOS/Linux):
|
|
120
123
|
|
|
121
124
|
```bash
|
|
125
|
+
PATCH_JSON=$(.vibe-x/agent-better-checkpoint/alloc_patch.sh --workspace "$PWD")
|
|
126
|
+
PATCH_PATH=$(printf '%s' "$PATCH_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["path"])')
|
|
127
|
+
# Write only the selected hunks for this logical unit to "$PATCH_PATH" as a unified diff patch.
|
|
122
128
|
.vibe-x/agent-better-checkpoint/checkpoint.sh \
|
|
123
129
|
"checkpoint(auth): add JWT token refresh logic
|
|
124
130
|
|
|
125
131
|
Implement automatic token refresh when access token expires.
|
|
126
132
|
Uses refresh token rotation for security." \
|
|
127
|
-
"帮我实现 token 刷新机制"
|
|
133
|
+
"帮我实现 token 刷新机制" \
|
|
134
|
+
--patch-file "$PATCH_PATH"
|
|
128
135
|
```
|
|
129
136
|
|
|
130
|
-
###
|
|
137
|
+
### Recommended patch flow (Windows):
|
|
131
138
|
|
|
132
139
|
```powershell
|
|
140
|
+
$patch = powershell -File ".vibe-x\agent-better-checkpoint\alloc_patch.ps1" -Workspace (Get-Location).Path | ConvertFrom-Json
|
|
141
|
+
# Write only the selected hunks for this logical unit to $patch.path as a unified diff patch.
|
|
133
142
|
powershell -File ".vibe-x\agent-better-checkpoint\checkpoint.ps1" `
|
|
134
143
|
"checkpoint(auth): add JWT token refresh logic`n`nImplement automatic token refresh when access token expires.`nUses refresh token rotation for security." `
|
|
144
|
+
"帮我实现 token 刷新机制" `
|
|
145
|
+
-PatchFile $patch.path
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Full-workspace fallback example (macOS/Linux):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
.vibe-x/agent-better-checkpoint/checkpoint.sh \
|
|
152
|
+
"checkpoint(auth): add JWT token refresh logic
|
|
153
|
+
|
|
154
|
+
Implement automatic token refresh when access token expires.
|
|
155
|
+
Uses refresh token rotation for security." \
|
|
135
156
|
"帮我实现 token 刷新机制"
|
|
136
157
|
```
|
|
137
158
|
|
|
138
159
|
### What the script does:
|
|
139
160
|
1. Truncates user prompt to ≤60 characters (head...tail)
|
|
140
161
|
2. Appends Git Trailers: `Agent`, `Checkpoint-Type`, `User-Prompt`
|
|
141
|
-
3.
|
|
142
|
-
4.
|
|
162
|
+
3. Either runs `git add -A && git commit` or applies the selected patch hunks and then commits
|
|
163
|
+
4. Returns `ABC_PATCH_*` errors when the selected patch cannot be committed safely
|
|
164
|
+
5. Exits gracefully if there are no changes
|
|
143
165
|
|
|
144
166
|
---
|
|
145
167
|
|
|
@@ -167,4 +189,4 @@ This should feel natural — commit as you go, like any good developer.
|
|
|
167
189
|
|
|
168
190
|
---
|
|
169
191
|
|
|
170
|
-
**Version**: 0.3.
|
|
192
|
+
**Version**: 0.3.4
|