xtrm-cli 2.1.16 → 2.1.19
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/dist/index.cjs +810 -425
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/clean.ts +323 -0
- package/src/utils/atomic-config.ts +123 -2
- package/test/atomic-config.test.ts +55 -0
- package/test/hooks.test.ts +156 -52
package/test/hooks.test.ts
CHANGED
|
@@ -77,7 +77,7 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
|
|
|
77
77
|
);
|
|
78
78
|
expect(r.status).toBe(2);
|
|
79
79
|
const out = parseHookJson(r.stdout);
|
|
80
|
-
expect(out?.systemMessage).toContain('Bash
|
|
80
|
+
expect(out?.systemMessage).toContain('Bash restricted');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('allows safe Bash commands on protected branch', () => {
|
|
@@ -117,10 +117,19 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
|
|
|
117
117
|
);
|
|
118
118
|
expect(r.status, `expected exit 2 for: ${command}`).toBe(2);
|
|
119
119
|
const out = parseHookJson(r.stdout);
|
|
120
|
-
expect(out?.systemMessage).toContain('Bash
|
|
120
|
+
expect(out?.systemMessage).toContain('Bash restricted');
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it('allows touch .beads/.memory-gate-done on protected branch', () => {
|
|
125
|
+
const r = runHook(
|
|
126
|
+
'main-guard.mjs',
|
|
127
|
+
{ tool_name: 'Bash', tool_input: { command: 'touch .beads/.memory-gate-done' } },
|
|
128
|
+
{ MAIN_GUARD_PROTECTED_BRANCHES: CURRENT_BRANCH },
|
|
129
|
+
);
|
|
130
|
+
expect(r.status).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
124
133
|
it('allows Bash when MAIN_GUARD_ALLOW_BASH=1 is set', () => {
|
|
125
134
|
const r = runHook(
|
|
126
135
|
'main-guard.mjs',
|
|
@@ -187,9 +196,11 @@ describe('main-guard-post-push.mjs', () => {
|
|
|
187
196
|
repoDir,
|
|
188
197
|
);
|
|
189
198
|
expect(r.status).toBe(0);
|
|
190
|
-
|
|
191
|
-
expect(
|
|
192
|
-
|
|
199
|
+
expect(r.stdout).toContain('gh pr create --fill');
|
|
200
|
+
expect(r.stdout).toContain('gh pr merge --squash');
|
|
201
|
+
// output must be plain text (agent-only), not a JSON systemMessage banner
|
|
202
|
+
expect(parseHookJson(r.stdout)).toBeNull();
|
|
203
|
+
|
|
193
204
|
} finally {
|
|
194
205
|
rmSync(repoDir, { recursive: true, force: true });
|
|
195
206
|
}
|
|
@@ -320,7 +331,7 @@ exit 1
|
|
|
320
331
|
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
321
332
|
);
|
|
322
333
|
expect(r.status).toBe(2);
|
|
323
|
-
expect(r.stderr).toContain('
|
|
334
|
+
expect(r.stderr).toContain('active claim');
|
|
324
335
|
} finally {
|
|
325
336
|
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
326
337
|
rmSync(projectDir, { recursive: true, force: true });
|
|
@@ -445,7 +456,7 @@ exit 1
|
|
|
445
456
|
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
446
457
|
);
|
|
447
458
|
expect(r.status).toBe(2);
|
|
448
|
-
expect(r.stderr).toContain('
|
|
459
|
+
expect(r.stderr).toContain('Memory gate');
|
|
449
460
|
} finally {
|
|
450
461
|
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
451
462
|
rmSync(projectDir, { recursive: true, force: true });
|
|
@@ -542,51 +553,6 @@ exit 2
|
|
|
542
553
|
});
|
|
543
554
|
|
|
544
555
|
|
|
545
|
-
// ── gitnexus-impact-reminder.py ──────────────────────────────────────────────
|
|
546
|
-
|
|
547
|
-
function runPythonHook(
|
|
548
|
-
hookFile: string,
|
|
549
|
-
input: Record<string, unknown>,
|
|
550
|
-
) {
|
|
551
|
-
return spawnSync('python3', [path.join(HOOKS_DIR, hookFile)], {
|
|
552
|
-
input: JSON.stringify(input),
|
|
553
|
-
encoding: 'utf8',
|
|
554
|
-
env: { ...process.env },
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
describe('gitnexus-impact-reminder.py', () => {
|
|
559
|
-
it('injects additionalContext when prompt contains an edit-intent keyword', () => {
|
|
560
|
-
const r = runPythonHook('gitnexus-impact-reminder.py', {
|
|
561
|
-
hook_event_name: 'UserPromptSubmit',
|
|
562
|
-
prompt: 'fix the broken auth logic in login.ts',
|
|
563
|
-
});
|
|
564
|
-
expect(r.status).toBe(0);
|
|
565
|
-
const out = parseHookJson(r.stdout);
|
|
566
|
-
expect(out?.hookSpecificOutput?.additionalContext).toContain('gitnexus impact');
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
it('does nothing (no output) when prompt has no edit-intent keywords', () => {
|
|
570
|
-
const r = runPythonHook('gitnexus-impact-reminder.py', {
|
|
571
|
-
hook_event_name: 'UserPromptSubmit',
|
|
572
|
-
prompt: 'explain how the beads gate works',
|
|
573
|
-
});
|
|
574
|
-
expect(r.status).toBe(0);
|
|
575
|
-
expect(r.stdout.trim()).toBe('');
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('does nothing for non-UserPromptSubmit events', () => {
|
|
579
|
-
const r = runPythonHook('gitnexus-impact-reminder.py', {
|
|
580
|
-
hook_event_name: 'PreToolUse',
|
|
581
|
-
tool_name: 'Edit',
|
|
582
|
-
tool_input: { file_path: 'foo.ts' },
|
|
583
|
-
prompt: 'fix something',
|
|
584
|
-
});
|
|
585
|
-
expect(r.status).toBe(0);
|
|
586
|
-
expect(r.stdout.trim()).toBe('');
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
|
|
590
556
|
|
|
591
557
|
// ── beads-gate-core.mjs — decision functions ──────────────────────────────────
|
|
592
558
|
|
|
@@ -704,3 +670,141 @@ process.exit(ok ? 0 : 1);
|
|
|
704
670
|
});
|
|
705
671
|
});
|
|
706
672
|
});
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
// ── beads-compact-save.mjs ───────────────────────────────────────────────────
|
|
676
|
+
describe('beads-compact-save.mjs', () => {
|
|
677
|
+
it('exits 0 silently when no .beads directory exists', () => {
|
|
678
|
+
const r = runHook('beads-compact-save.mjs', { hook_event_name: 'PreCompact', cwd: '/tmp' });
|
|
679
|
+
expect(r.status).toBe(0);
|
|
680
|
+
expect(r.stdout).toBe('');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('exits 0 silently and writes no file when no in_progress issues', () => {
|
|
684
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-save-'));
|
|
685
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
686
|
+
const fake = withFakeBdDir(`#!/usr/bin/env bash
|
|
687
|
+
if [[ "$1" == "list" ]]; then
|
|
688
|
+
echo ""
|
|
689
|
+
echo "--------------------------------------------------------------------------------"
|
|
690
|
+
echo "Total: 2 issues (2 open, 0 in progress)"
|
|
691
|
+
exit 0
|
|
692
|
+
fi
|
|
693
|
+
exit 1
|
|
694
|
+
`);
|
|
695
|
+
try {
|
|
696
|
+
const r = runHook(
|
|
697
|
+
'beads-compact-save.mjs',
|
|
698
|
+
{ hook_event_name: 'PreCompact', cwd: projectDir },
|
|
699
|
+
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
700
|
+
);
|
|
701
|
+
expect(r.status).toBe(0);
|
|
702
|
+
const { existsSync } = require('node:fs');
|
|
703
|
+
expect(existsSync(path.join(projectDir, '.beads', '.last_active'))).toBe(false);
|
|
704
|
+
} finally {
|
|
705
|
+
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
706
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('writes .beads/.last_active with in_progress issue IDs', () => {
|
|
711
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-save-'));
|
|
712
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
713
|
+
const fake = withFakeBdDir(`#!/usr/bin/env bash
|
|
714
|
+
if [[ "$1" == "list" ]]; then
|
|
715
|
+
cat <<'EOF'
|
|
716
|
+
◐ proj-abc123 ● P1 First in_progress issue
|
|
717
|
+
◐ proj-def456 ● P2 Second in_progress issue
|
|
718
|
+
|
|
719
|
+
--------------------------------------------------------------------------------
|
|
720
|
+
Total: 2 issues (0 open, 2 in progress)
|
|
721
|
+
EOF
|
|
722
|
+
exit 0
|
|
723
|
+
fi
|
|
724
|
+
exit 1
|
|
725
|
+
`);
|
|
726
|
+
try {
|
|
727
|
+
const r = runHook(
|
|
728
|
+
'beads-compact-save.mjs',
|
|
729
|
+
{ hook_event_name: 'PreCompact', cwd: projectDir },
|
|
730
|
+
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
731
|
+
);
|
|
732
|
+
expect(r.status).toBe(0);
|
|
733
|
+
const { readFileSync: rfs } = require('node:fs');
|
|
734
|
+
const saved = rfs(path.join(projectDir, '.beads', '.last_active'), 'utf8').trim().split('\n');
|
|
735
|
+
expect(saved).toContain('proj-abc123');
|
|
736
|
+
expect(saved).toContain('proj-def456');
|
|
737
|
+
} finally {
|
|
738
|
+
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
739
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// ── beads-compact-restore.mjs ────────────────────────────────────────────────
|
|
745
|
+
describe('beads-compact-restore.mjs', () => {
|
|
746
|
+
it('exits 0 silently when no .beads/.last_active file exists', () => {
|
|
747
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-restore-'));
|
|
748
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
749
|
+
try {
|
|
750
|
+
const r = runHook(
|
|
751
|
+
'beads-compact-restore.mjs',
|
|
752
|
+
{ hook_event_name: 'SessionStart', cwd: projectDir },
|
|
753
|
+
);
|
|
754
|
+
expect(r.status).toBe(0);
|
|
755
|
+
expect(r.stdout).toBe('');
|
|
756
|
+
} finally {
|
|
757
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('restores in_progress status, deletes .last_active, and injects additionalSystemPrompt', () => {
|
|
762
|
+
const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-restore-'));
|
|
763
|
+
mkdirSync(path.join(projectDir, '.beads'));
|
|
764
|
+
const { writeFileSync: wfs } = require('node:fs');
|
|
765
|
+
wfs(path.join(projectDir, '.beads', '.last_active'), 'proj-abc123\nproj-def456\n', 'utf8');
|
|
766
|
+
|
|
767
|
+
const callLog = path.join(projectDir, 'bd-calls.log');
|
|
768
|
+
const fake = withFakeBdDir(`#!/usr/bin/env bash
|
|
769
|
+
echo "$@" >> "${callLog}"
|
|
770
|
+
exit 0
|
|
771
|
+
`);
|
|
772
|
+
try {
|
|
773
|
+
const r = runHook(
|
|
774
|
+
'beads-compact-restore.mjs',
|
|
775
|
+
{ hook_event_name: 'SessionStart', cwd: projectDir },
|
|
776
|
+
{ PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
|
|
777
|
+
);
|
|
778
|
+
expect(r.status).toBe(0);
|
|
779
|
+
// .last_active must be deleted
|
|
780
|
+
const { existsSync: exs, readFileSync: rfs } = require('node:fs');
|
|
781
|
+
expect(exs(path.join(projectDir, '.beads', '.last_active'))).toBe(false);
|
|
782
|
+
// bd update called for each ID
|
|
783
|
+
const calls = rfs(callLog, 'utf8');
|
|
784
|
+
expect(calls).toContain('proj-abc123');
|
|
785
|
+
expect(calls).toContain('proj-def456');
|
|
786
|
+
// additionalSystemPrompt injected for agent
|
|
787
|
+
const out = parseHookJson(r.stdout);
|
|
788
|
+
expect(out?.hookSpecificOutput?.additionalSystemPrompt).toMatch(/Restored 2 in_progress issue/);
|
|
789
|
+
} finally {
|
|
790
|
+
rmSync(fake.tempDir, { recursive: true, force: true });
|
|
791
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
// ── hooks.json wiring ────────────────────────────────────────────────────────
|
|
798
|
+
describe('hooks.json — beads-compact hooks wiring', () => {
|
|
799
|
+
it('wires beads-compact-save.mjs to PreCompact event', () => {
|
|
800
|
+
const cfg = JSON.parse(readFileSync(path.join(__dirname, '../../config/hooks.json'), 'utf8'));
|
|
801
|
+
const preCompact: Array<{ script: string }> = cfg.hooks.PreCompact ?? [];
|
|
802
|
+
expect(preCompact.some((h) => h.script === 'beads-compact-save.mjs')).toBe(true);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('wires beads-compact-restore.mjs to SessionStart event', () => {
|
|
806
|
+
const cfg = JSON.parse(readFileSync(path.join(__dirname, '../../config/hooks.json'), 'utf8'));
|
|
807
|
+
const sessionStart: Array<{ script: string }> = cfg.hooks.SessionStart ?? [];
|
|
808
|
+
expect(sessionStart.some((h) => h.script === 'beads-compact-restore.mjs')).toBe(true);
|
|
809
|
+
});
|
|
810
|
+
});
|